<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: ivan chiou</title>
    <description>The latest articles on Forem by ivan chiou (@ivanchiou).</description>
    <link>https://forem.com/ivanchiou</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844634%2F08b1a548-a9f3-4c0f-9056-39a485d5750b.jpeg</url>
      <title>Forem: ivan chiou</title>
      <link>https://forem.com/ivanchiou</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ivanchiou"/>
    <language>en</language>
    <item>
      <title>CI/CD pipeline using GitHub Actions to deploy to Google Cloud Platform (GCP)</title>
      <dc:creator>ivan chiou</dc:creator>
      <pubDate>Wed, 30 Apr 2025 01:21:58 +0000</pubDate>
      <link>https://forem.com/ivanchiou/cicd-pipeline-using-github-actions-to-deploy-to-google-cloud-platform-gcp-3105</link>
      <guid>https://forem.com/ivanchiou/cicd-pipeline-using-github-actions-to-deploy-to-google-cloud-platform-gcp-3105</guid>
      <description>&lt;h2&gt;
  
  
  🚀 Why CI/CD?
&lt;/h2&gt;

&lt;p&gt;CI/CD automates every stage of your software delivery process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI (Continuous Integration)&lt;/strong&gt;: Automatically builds and tests your code on each commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CD (Continuous Deployment)&lt;/strong&gt;: Automatically deploys your application to the production environment after passing tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, CI/CD reduces errors, improves productivity, and ensures reliable releases—critical in agile and DevOps workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ Project Overview
&lt;/h2&gt;

&lt;p&gt;You’ll learn how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use GitHub Actions to automate Maven build and GCP deployment.&lt;/li&gt;
&lt;li&gt;Securely manage SSH keys and secrets.&lt;/li&gt;
&lt;li&gt;Use Docker for containerization (optional).&lt;/li&gt;
&lt;li&gt;Deploy to a GCP Compute Engine VM running Ubuntu.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔧 Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you begin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GCP project with Compute Engine VM set up (Ubuntu OS, Java installed).&lt;/li&gt;
&lt;li&gt;GitHub repository for your Spring Boot project.&lt;/li&gt;
&lt;li&gt;Domain name (e.g., from No-IP) and optional SSL certificate via Let’s Encrypt.&lt;/li&gt;
&lt;li&gt;SSH key pair added to your GitHub and GCP metadata.&lt;/li&gt;
&lt;li&gt;Application setup (e.g., &lt;code&gt;application.properties&lt;/code&gt;, MySQL, Redis, etc.) already running on the VM.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📁 GitHub Repository Setup
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjsspwldznyb4l60t2ie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjsspwldznyb4l60t2ie.png" alt="Image description" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add Secrets to GitHub&lt;/strong&gt;:
Navigate to &lt;code&gt;Settings → Secrets and Variables → Actions&lt;/code&gt;, and add:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GCP_SSH_PRIVATE_KEY&lt;/code&gt;: Your private SSH key (no passphrase).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GCP_VM_IP&lt;/code&gt;: Your VM’s external IP.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GCP_VM_USER&lt;/code&gt;: The SSH username (usually your GCP email-based user).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KEYSTORE_BASE64&lt;/code&gt;: Base64 encoded &lt;code&gt;keystore.p12&lt;/code&gt; SSL cert file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To create &lt;code&gt;KEYSTORE_BASE64&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;base64 &lt;/span&gt;cert/keystore.p12 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; keystore_base64.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Public Key in GCP VM&lt;/strong&gt;:
Upload your public SSH key (&lt;code&gt;gcp_ssh_key.pub&lt;/code&gt;) using GCP’s OS Login or manually via &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  📜 GitHub Actions Workflow File (&lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;)
&lt;/h2&gt;



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

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up JDK &lt;/span&gt;&lt;span class="m"&gt;23&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temurin'&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;23'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build with Maven&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mvn clean package -DskipTests&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up SSH&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p ~/.ssh&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.GCP_SSH_PRIVATE_KEY }}" &amp;gt; ~/.ssh/id_rsa&lt;/span&gt;
          &lt;span class="s"&gt;chmod 600 ~/.ssh/id_rsa&lt;/span&gt;
          &lt;span class="s"&gt;ssh-keyscan -H ${{ secrets.GCP_VM_IP }} &amp;gt;&amp;gt; ~/.ssh/known_hosts&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy JAR to GCP VM&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;scp -i ~/.ssh/id_rsa target/*.jar ${{ secrets.GCP_VM_USER }}@${{ secrets.GCP_VM_IP }}:/home/${{ secrets.GCP_VM_USER }}/app.jar&lt;/span&gt;
          &lt;span class="s"&gt;ssh -i ~/.ssh/id_rsa ${{ secrets.GCP_VM_USER }}@${{ secrets.GCP_VM_IP }} &amp;lt;&amp;lt; 'EOF'&lt;/span&gt;
            &lt;span class="s"&gt;sudo pkill -f "java -jar" || true&lt;/span&gt;
            &lt;span class="s"&gt;nohup java -jar /home/${{ secrets.GCP_VM_USER }}/app.jar --spring.config.location=/home/${{ secrets.GCP_VM_USER }}/application.properties &amp;gt; app.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/span&gt;
          &lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔐 SSL and Secrets Management
&lt;/h2&gt;

&lt;p&gt;Use Let’s Encrypt to generate a free SSL certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;certbot
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot certonly &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; your-domain.ddns.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Convert to &lt;code&gt;keystore.p12&lt;/code&gt; for Spring Boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl pkcs12 &lt;span class="nt"&gt;-export&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; fullchain.pem &lt;span class="nt"&gt;-inkey&lt;/span&gt; privkey.pem &lt;span class="nt"&gt;-out&lt;/span&gt; keystore.p12 &lt;span class="nt"&gt;-name&lt;/span&gt; mysslkey &lt;span class="nt"&gt;-password&lt;/span&gt; pass:yourpassword
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Encode and add it to GitHub as &lt;code&gt;KEYSTORE_BASE64&lt;/code&gt;, then restore it in your workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restore keystore.p12&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;mkdir -p src/main/resources/cert&lt;/span&gt;
    &lt;span class="s"&gt;echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode &amp;gt; src/main/resources/cert/keystore.p12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🐳 Bonus: Dockerizing Your Java App
&lt;/h2&gt;

&lt;p&gt;To containerize your app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a &lt;code&gt;Dockerfile&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; openjdk:17-slim&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; target/*.jar app.jar&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["java", "-jar", "app.jar"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Add Docker build/push to your GitHub Actions:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Docker Image&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t youruser/demo-spring-app .&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Push to Docker Hub&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin&lt;/span&gt;
    &lt;span class="s"&gt;docker push youruser/demo-spring-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Pull and run the container on GCP VM:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull youruser/demo-spring-app
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; demo &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 youruser/demo-spring-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ✅ CI/CD Flow Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Developer pushes code to GitHub &lt;code&gt;main&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;GitHub Actions:

&lt;ul&gt;
&lt;li&gt;Builds the project using Maven.&lt;/li&gt;
&lt;li&gt;Deploys &lt;code&gt;.jar&lt;/code&gt; or Docker image to GCP VM.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;App runs on VM, accessible via your domain and secured with SSL.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🎯 Conclusion
&lt;/h2&gt;

&lt;p&gt;Setting up a CI/CD pipeline with GitHub Actions and GCP Compute Engine offers a powerful, scalable, and fully automated deployment workflow for your Java apps. By leveraging GitHub’s native integration with Secrets and Actions, plus the flexibility of GCP, you can deploy with confidence and consistency.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Implementing a Virtual-Physical Environment Manipulation System based on ROS using Python and Three.js</title>
      <dc:creator>ivan chiou</dc:creator>
      <pubDate>Sun, 14 Jan 2024 06:19:46 +0000</pubDate>
      <link>https://forem.com/ivanchiou/implementing-a-virtual-physical-environment-manipulation-system-based-on-ros-using-python-and-threejs-50lp</link>
      <guid>https://forem.com/ivanchiou/implementing-a-virtual-physical-environment-manipulation-system-based-on-ros-using-python-and-threejs-50lp</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx16touen40gcy76b2v6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx16touen40gcy76b2v6c.png" alt="the architecture of the Virtual-Physical Environment system" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's explore the architecture of the Virtual-Physical Environment system. On the frontend, we utilize the Vue framework and Three.js for visualizing our virtual world, encompassing a robotic arm and a 3D environment.&lt;/p&gt;

&lt;p&gt;Moving to the middle layer, the backend employs Python to establish a connection between the frontend through WebSockets and the physical world via Rosbridge.&lt;/p&gt;

&lt;p&gt;In the third section, which is the local side, we have the robotic arm and IP camera. The robotic arm, based on ROS, receives messages from Rosbridge, while the IP camera covers and monitors the physical environment. The captured data is then sent to the frontend for display on pages using WebRTC.&lt;/p&gt;

&lt;p&gt;The demonstrations are presented as follows:&lt;br&gt;
&lt;a href="https://www.youtube.com/watch?v=XKHiDOqjk7Y"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8qpxjv60spqhei3um4d.png" alt="User Interface of the Virtual-Physical Environment system" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The JavaScript code for receiving ROS messages from both the physical and virtual robotic arms is as follows:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcot9iv0fzgeum2wudbnk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcot9iv0fzgeum2wudbnk.png" alt="JavaScript code for receiving ROS messages" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The messages are displayed on the console of the physical robotic arm.&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fohc13goo31nifgix4to9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fohc13goo31nifgix4to9.png" alt="the console of the physical robotic arm" width="800" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The messages are printed on the console of the client webpages.&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1i3gb6lms102oyfc7sfm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1i3gb6lms102oyfc7sfm.png" alt="the console of the client webpages" width="800" height="709"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The entire interaction flow between the virtual (Unity and Three.js) and physical environments of the robotic arm based on ROS, facilitated by ROS TCP Connector to Unity and ROSBridge to Python, is as follows:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3mpjzddu3b6950aelghq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3mpjzddu3b6950aelghq.png" alt="interaction flow between the virtual and physical environments" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The topic will be presented at the upcoming &lt;a href="https://www.conf42.com/Python_2024_Ivan_Chiou_virtualphysical_ros_python_threejs"&gt;Conf42&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Dockerizing a project using php Laravel, Composer, Artisan and Laradock</title>
      <dc:creator>ivan chiou</dc:creator>
      <pubDate>Fri, 05 Jan 2024 10:33:11 +0000</pubDate>
      <link>https://forem.com/ivanchiou/dockerizing-a-project-using-php-laravel-composer-artisan-and-laradock-l2e</link>
      <guid>https://forem.com/ivanchiou/dockerizing-a-project-using-php-laravel-composer-artisan-and-laradock-l2e</guid>
      <description>&lt;p&gt;Mastering how to dockerize everything is a good starting point for any project.&lt;/p&gt;

&lt;p&gt;Initially, you must install the PHP executable environment. (My OS is windows using Chocolatey cli)&lt;br&gt;
&lt;code&gt;choco install php --version=7.3.0&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Next, proceed to install Composer using the Chocolatey CLI.&lt;br&gt;
&lt;code&gt;choco install composer --version=2.1.14&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now, you can utilize the Composer command to create the Laravel project.&lt;br&gt;
&lt;code&gt;composer create-project --prefer-dist laravel/laravel laraveldock "7.*.*"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It will auo-create a laravel project with composer which is similar as npm in JS, and with artisan which is a command-based interface to develop your app easily. For example, you can launch server by&lt;br&gt;
&lt;code&gt;php artisan serve&lt;/code&gt;&lt;br&gt;
and synchronize the database with the migration version.&lt;br&gt;
&lt;code&gt;php artisan migrate&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The folder structure of the project created by Composer is as follows:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VlaF1o0n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ommq93yzfrctn285yz8j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VlaF1o0n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ommq93yzfrctn285yz8j.png" alt="Image description" width="213" height="759"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next step is to create a dockerized app using docker-compose in &lt;a href="https://laradock.io/"&gt;Laradock&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Firstly, you should add the Laradock repository as a submodule in your Laravel project.&lt;br&gt;
&lt;code&gt;git submodule add https://github.com/Laradock/laradock.git laradock&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bKq8mO_C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0szw6qvssgfe2fsh7ljx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bKq8mO_C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0szw6qvssgfe2fsh7ljx.png" alt="Image description" width="338" height="653"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, go into the Laradock folder.&lt;br&gt;
&lt;code&gt;cd laradock&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Rename the default environment file to .env.&lt;br&gt;
&lt;code&gt;mv env-example .env&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Execute the docker-compose command as follows:&lt;br&gt;
&lt;code&gt;docker-compose up -d --build nginx&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you encounter the below issue,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to fetch http://deb.debian.org/debian/dists/bookworm/InRelease Could not connect to deb.debian.org:80 (127.0.0.1). - connect (111: Connection refused)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;try to add this configuration into daemon.json of docker.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{&lt;br&gt;
    "dns": ["8.8.8.8"]&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Finally, it is done.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TOu-VyXR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rytf1apoltux7s4m3md5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TOu-VyXR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rytf1apoltux7s4m3md5.png" alt="Image description" width="552" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tVD0O4hJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yw3xjtoybj0d04nk8l26.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tVD0O4hJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yw3xjtoybj0d04nk8l26.png" alt="Image description" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type 127.0.0.1 into your browser, then you will be directed to the front page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DIOiXJvt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wx5nnkdbe3qgdug75af4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DIOiXJvt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wx5nnkdbe3qgdug75af4.png" alt="Image description" width="660" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you prefer not to use Laradock, you can create a Dockerfile based on the php:7.4-apache image by your own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM php:7.4-apache
RUN docker-php-ext-install pdo_mysql
RUN apt-get update &amp;amp;&amp;amp; \
    apt-get upgrade -y &amp;amp;&amp;amp; \
    apt-get install -y git
RUN apt-get install zip unzip
RUN apt-get install curl &amp;amp;&amp;amp; \
  curl -sS https://getcomposer.org/installer | php \
  &amp;amp;&amp;amp; chmod +x composer.phar &amp;amp;&amp;amp; mv composer.phar /usr/local/bin/composer
RUN apt-get install -y libpng-dev
RUN apt-get install -y zlib1g-dev
RUN docker-php-ext-install mysqli
WORKDIR /var/www/html
CMD bash -c "composer install &amp;amp;&amp;amp; chmod 777 storage &amp;amp;&amp;amp; chmod 777 -R bootstrap/cache &amp;amp;&amp;amp; php artisan key:generate &amp;amp;&amp;amp; php artisan storage:link"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>php</category>
      <category>laravel</category>
      <category>docker</category>
      <category>composer</category>
    </item>
    <item>
      <title>How to implement a microservices architecture for an AI learning platform using Python and Kubernetes</title>
      <dc:creator>ivan chiou</dc:creator>
      <pubDate>Thu, 04 Jan 2024 09:44:32 +0000</pubDate>
      <link>https://forem.com/ivanchiou/how-to-implement-a-microservices-architecture-for-an-ai-learning-platform-using-python-and-kubernetes-1hi9</link>
      <guid>https://forem.com/ivanchiou/how-to-implement-a-microservices-architecture-for-an-ai-learning-platform-using-python-and-kubernetes-1hi9</guid>
      <description>&lt;p&gt;The microservices architecture of the PAIA system, an AI learning platform that encourages student competition through AI game code writing, offers three key solutions: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Seamless integration and independent control of Python language with containerized apps in our AI training system.&lt;/li&gt;
&lt;li&gt;Independent microservices that can be updated separately.&lt;/li&gt;
&lt;li&gt;Rapid configuration of environment parameters based ondiverse development scenarios.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our PAIA system is built on the Django framework using Python, where Django is primarily responsible for managing the Application Programming Interface (API), encompassing authentication, authorization, and accounting features connected to a PostgreSQL database. PAIA aims to provide an Artificial Intelligence (AI) learning and competitive environment based on gaming or edutainment methodology. Users can code their AI using Blockly, a block-based visual programming language, or Python, and then upload it to run their AI models and training data on our platform.&lt;/p&gt;

&lt;p&gt;The entire system is divided into three components:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CpyVvGyY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ahbgbsskdyt98po77vu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CpyVvGyY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ahbgbsskdyt98po77vu5.png" alt="Image description" width="690" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[Frontend (Vue)] &amp;lt;-&amp;gt; [Backend (Django)] &amp;lt;-&amp;gt; [MLGame (Python/Pygame)]&lt;/p&gt;

&lt;p&gt;Both the backend and MLGame, excluding the frontend, are developed using Python and related skills. The details of their implementation and architecture are as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kil0SM8f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8s1mysitas6qa5xx53hl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kil0SM8f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8s1mysitas6qa5xx53hl.png" alt="Image description" width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
Backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API design&lt;/li&gt;
&lt;li&gt;Serializer and folder structure in Django&lt;/li&gt;
&lt;li&gt;Database creation and migration&lt;/li&gt;
&lt;li&gt;Redis and cache mechanism&lt;/li&gt;
&lt;li&gt;Pika and WebSocket integration&lt;/li&gt;
&lt;li&gt;AAA (authentication, authorization, accounting) design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kSdR8PV_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/irieedom62j7xuy4caki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kSdR8PV_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/irieedom62j7xuy4caki.png" alt="Image description" width="769" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MLGame (Game Core, Game, AI, and Game Data):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MLGame, authored by Kun-Yi Li and available on &lt;a href="https://github.com/LanKuDot/MLGame"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RabbitMQ consumer (Pika) with WebSocket&lt;/li&gt;
&lt;li&gt;Kubernetes client API in Python&lt;/li&gt;
&lt;li&gt;Python multiple inheritance and design patterns in AI fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main purpose of MLGame is to run games created by creators through the Pygame framework. For a game to be executable within the MLGame framework, it needs to adhere to the structure specified by MLGame. In other words, MLGame can be considered as a game engine with a focus on machine learning.&lt;/p&gt;

&lt;p&gt;It all starts with the program MLGame.py.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def __init__(self, propty: GameMLModeExecutorProperty):
    self._execution_cmd = propty.execution_cmd

    # Get the active ml names from the created ml processes
    self._active_ml_names = list(self._comm_manager.get_ml_names())
    self._recorder = get_recorder(self._execution_cmd, self._ml_names)
    self._progress_recorder = get_progress_recorder(self._execution_cmd, self._ml_names)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By executing the following command in the command prompt (cmd), a game can be launched.&lt;br&gt;
&lt;code&gt;python MLGame.py -i ml_play_template.py easy_game  --score 10 --color FF9800 --time_to_play 1200 --total_point 15&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Among them, ml_play_template.py is an AI program written by the player. The game to be executed is named "easy_game," followed by the configuration parameters for that game.&lt;/p&gt;

&lt;p&gt;Before running the game, we use an external program to create the MLGame container. The program is responsible for using Pika to listen to commands from the API (Backend).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connection = pika.BlockingConnection(
    pika.ConnectionParameters(host=MQ_HOST, credentials=MQ_CREDENTIAL),
)
channel = connection.channel()

channel.queue_declare(queue=queue_name, durable=True)
channel.basic_publish(exchange='', routing_key=queue_name, body=game_cmd,
            properties=pika.BasicProperties(delivery_mode=2))

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

&lt;/div&gt;



&lt;p&gt;After packaging the game to be executed as a Docker container, the container will be deleted upon completion of the execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import docker
container_client.from_env()
container = container_client.run(target_image, 
                                 ["tail","-f","/dev/null"], 
                                 name=container_name, 
                                 detach=True, 
                                 auto_remove=False,
                                 mem_limit=memory_limit,
                                 network="bridge",
                                 stdin_open=True,tty=True)

container.exec_run(mlgame_cmd,detach=True,stdin=True,tty=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, it has been modified to use pods in Kubernetes to encapsulate containers with auto-deletion after execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from kubernetes import client, config as k8s_config, utils
from kubernetes.client import Configuration
from kubernetes.client.api import core_v1_api
api_response = api_instance.read_namespaced_pod_log(name=POD_NAME, namespace='default')

with open(os.path.join(os.path.dirname(__file__), "/root/pod.yaml")) as f:
    pod_body = yaml.safe_load(f)
    api_instance.create_namespaced_pod(namespace='default', body=pod_body)

api_instance.patch_namespaced_pod(name=POD_NAME, namespace='default', body={"metadata":{"labels":{"name": "mlgame-completed-pod“}}})

api_instance.delete_namespaced_pod(name=POD_NAME, namespace='default')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Following this, we proceed to download the relevant AI program, ml_play_template.py. After downloading the AI program, we use the subprocess to open another thread to execute the game.&lt;br&gt;
&lt;code&gt;process = subprocess.Popen(mlgame_cmd, stdout=subprocess.PIPE, universal_newlines=True)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We can monitor MLGame containers through RabbitMQ to ensure that their tasks have been completed.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OlUeJTai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ldpfptm7m9url5bwdu3s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OlUeJTai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ldpfptm7m9url5bwdu3s.png" alt="Image description" width="800" height="344"&gt;&lt;/a&gt;&lt;br&gt;
ASGI in Django API service facilitates the handling of messages from MLGame through websocket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"websocket": TokenAuthMiddlewareStack(
    URLRouter(
        [
          path("ws/game_room/&amp;lt;str:room_id&amp;gt;",GameRoomConsumer.as_asgi())
        ]
    )
),

from channels.generic.websocket import AsyncWebsocketConsumer

self.accept()
self.send(text_data=message)
async def receive(self, text_data)
await self.channel_layer.group_send(self.group_name, {'type': 'chat_message’, 'message': recv_data})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within the &lt;strong&gt;GameMLModeExecutor&lt;/strong&gt;, the game loop is executed. The &lt;strong&gt;send_game_progress&lt;/strong&gt; function is employed to send data to the backend via WebSocket. This data includes the coordinates, dimensions, colors, status (player info), and time of the foreground, background, and objects within the game. The backend then forwards this information to the client, rendering it in the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def _loop(self):
        game = self._game_cls(*self._execution_cmd.game_params)
        assert isinstance(game, PaiaGame), "Game " + str(game) + " should implement a abstract class : PaiaGame"
        scene_init_info_dict = game.get_scene_init_data()
        # game_view = PygameView(scene_init_info_dict)
        while not quit_or_esc():
            cmd_dict = game.get_keyboard_command()
            result = game.update(cmd_dict)
            view_data = game.get_scene_progress_data()
            # game_view.draw(view_data)
            if result == "QUIT":
                scene_info_dict = game.game_to_player_data()
                self._recorder.record(scene_info_dict, {})
                self._recorder.flush_to_file()
                game.reset()
                break
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the end, after deploying a Kubernetes Cluster using Jenkins for product deployment, we can play the game and observe its animation in the frontend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RQDhRFDC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmanwmqgpoz0knhjgvf2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RQDhRFDC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmanwmqgpoz0knhjgvf2.png" alt="Image description" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--geEiPMxx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5lti4ix1gdhhmj2zw25t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--geEiPMxx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5lti4ix1gdhhmj2zw25t.png" alt="Image description" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>kubernetes</category>
      <category>docker</category>
      <category>websocket</category>
    </item>
    <item>
      <title>Medusa storefront with multiple vendors</title>
      <dc:creator>ivan chiou</dc:creator>
      <pubDate>Thu, 02 Jun 2022 02:18:47 +0000</pubDate>
      <link>https://forem.com/ivanchiou/medusa-storefront-with-multiple-vendors-23jp</link>
      <guid>https://forem.com/ivanchiou/medusa-storefront-with-multiple-vendors-23jp</guid>
      <description>&lt;p&gt;Medusa is open-source project which provides many powerful e-commerce functions and extensions as Shopify. I follows the &lt;a href="https://dev.to/medusajs/create-an-open-source-commerce-marketplace-part-1-3m5k"&gt;article for multi-vendor marketplaces&lt;/a&gt; shared from Shahed Nasser who is very outstanding engineer in Medusa Dev community. But it only shows for backend modification with &lt;a href="https://github.com/adrien2p/medusa-extender"&gt;medusa-extender&lt;/a&gt;, not include the change of frontend side. &lt;/p&gt;

&lt;p&gt;So I start to try to figure out how to modify the part of store frontend, but first we need to customize the original APIs for all vendors.&lt;/p&gt;

&lt;p&gt;At first, following all steps from the &lt;a href="https://dev.to/medusajs/create-an-open-source-commerce-marketplace-part-1-3m5k"&gt;article for multi-vendor marketplaces&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Second, add the &lt;strong&gt;store.router.ts&lt;/strong&gt; file as below. In where, I refined the client API '/store/products' for all vendors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { MedusaAuthenticatedRequest, Router } from 'medusa-extender';
import { Response, NextFunction } from "express";
import { User } from '../../user/entities/user.entity';

@Router({
    routes: [{
        requiredAuth: false,
        path: '/store/products',
        method: 'get',
        handlers: [
            async (req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise&amp;lt;Response&amp;lt;User[]&amp;gt;&amp;gt; =&amp;gt; {
                const productService = req.scope.resolve("productService")
                const resProducts = await productService.list({},
                    {
                        relations: [
                            "variants",
                            "variants.prices",
                            "variants.options",
                            "options",
                            "options.values",
                            "images",
                            "tags",
                            "collection",
                            "type",
                        ],
                    });
                return res.send({
                    "products": resProducts,
                    "count": resProducts.length,
                    "offset": 0,
                    "limit": 100
                });
            }
        ]
    }] 
})
export class StoreRouter {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add &lt;strong&gt;StoreRouter&lt;/strong&gt; into your &lt;strong&gt;store.module.ts&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Module({
    imports: [Store, StoreRepository, StoreService, StoreRouter],
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easy way to skip the loggedInUser condition in &lt;strong&gt;product.service.ts&lt;/strong&gt; then the product list will be changed to query for all vendors. It looks like tricky but I didn't get other options to make multi-vendors workable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    prepareListQuery_(selector: object, config: object): object {
        /*const loggedInUser = this.container.loggedInUser
        if (loggedInUser) {
            selector['store_id'] = loggedInUser.store_id // this is for one store
        }*/

        return super.prepareListQuery_(selector, config);
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebuild and start backend in your local environment. Try to create a new vendor by postman.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FnhjFw36--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nz1uxena0avhoyu5ldz4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FnhjFw36--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nz1uxena0avhoyu5ldz4.png" alt="Image description" width="794" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;authenticate to this vendor then create a new product.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VHD7ldDR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jltara78z02oas3xjueo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VHD7ldDR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jltara78z02oas3xjueo.png" alt="Image description" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the end, all clients can query all products including the new product created by the new vendor, even the client didn't login yet.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Kfi7a574--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1qsnw1v10679cv8nrwuk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Kfi7a574--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1qsnw1v10679cv8nrwuk.png" alt="Image description" width="754" height="784"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The difficult part is for the modification of store frontend&lt;/strong&gt;. I use &lt;a href="https://github.com/medusajs/gatsby-starter-medusa"&gt;Medusa Gatsby Starter&lt;/a&gt; to my store frontend. However, there are many incompatible with my above modification of backend.&lt;/p&gt;

&lt;p&gt;When I deployed all modification on the production environment, the store frontend rebuilt as well. But it shows failure as below:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3oXEZIkP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h9xc42x7oww4y1oc0rns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3oXEZIkP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h9xc42x7oww4y1oc0rns.png" alt="Image description" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, I found the create node function(in the build time) from &lt;a href="https://www.gatsbyjs.com/plugins/gatsby-source-filesystem/"&gt;gatsby-source-filesystem of gatsby&lt;/a&gt; does not support to pass the empty link address of product thumbnail. You must give it a valid url.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--faakip0u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tnau6mw6jfq1r8ofv7zo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--faakip0u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tnau6mw6jfq1r8ofv7zo.png" alt="Image description" width="800" height="101"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;I manually updated it to &lt;a href="https://dev.to/ivanchiou/medusa-images-uploading-using-imgur-api-2hli"&gt;imgur images I created in my previous post&lt;/a&gt; in postgresql using &lt;a href="https://github.com/sosedoff/pgweb"&gt;pgweb tool&lt;/a&gt; then it rebuilt successfully. But it is not the last mission. I still found an error in gatsby-starter-medusa if the product doesn't have any uploaded images.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nd1tRITI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6l6v7t92kh3f9kfpwbo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nd1tRITI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6l6v7t92kh3f9kfpwbo.png" alt="Image description" width="800" height="439"&gt;&lt;/a&gt;&lt;br&gt;
So we need to add below condition to prevent from this error.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RD7Sjsl6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/51lrny38e47ymmul15f5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RD7Sjsl6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/51lrny38e47ymmul15f5.png" alt="Image description" width="800" height="64"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I think it is not efficient to debug simultaneously both on these open-source projects, gatsby and medusa. But I will keep fighting with open-sources:)&lt;/p&gt;

</description>
      <category>medusa</category>
      <category>javascript</category>
      <category>router</category>
      <category>products</category>
    </item>
    <item>
      <title>Medusa images uploading using Imgur API</title>
      <dc:creator>ivan chiou</dc:creator>
      <pubDate>Mon, 02 May 2022 03:51:23 +0000</pubDate>
      <link>https://forem.com/ivanchiou/medusa-images-uploading-using-imgur-api-2hli</link>
      <guid>https://forem.com/ivanchiou/medusa-images-uploading-using-imgur-api-2hli</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/medusajs/medusa" rel="noopener noreferrer"&gt;Medusa&lt;/a&gt; is very powerful, useful, open-source and easily-revised e-commerce project. &lt;/p&gt;

&lt;p&gt;But in default, it doesn't provide any kind free online space to store products' pictures. The offical documents describe how to use &lt;a href="https://docs.medusajs.com/how-to/uploading-images-to-spaces/" rel="noopener noreferrer"&gt;digitalOcean&lt;/a&gt; and &lt;a href="https://docs.medusajs.com/how-to/uploading-images-to-s3/" rel="noopener noreferrer"&gt;AWS S3&lt;/a&gt; to put your uploaded images of products but those services are used with paid. &lt;/p&gt;

&lt;p&gt;Eventually, I found one way to use &lt;a href="https://imgur.com/" rel="noopener noreferrer"&gt;Imgur&lt;/a&gt; API to store our images for free totally. Following are the steps you can go with.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;create imgur account then go to its &lt;a href="https://imgur.com/account/settings/apps" rel="noopener noreferrer"&gt;settings page&lt;/a&gt;. It will list client id and secrets key of apps as below capture.&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzsoojlnflotzn2ooc4l5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzsoojlnflotzn2ooc4l5.png" alt="Image description"&gt;&lt;/a&gt;&lt;br&gt;
If you don't have any apps yet, &lt;a href="https://api.imgur.com/oauth2/addclient" rel="noopener noreferrer"&gt;create one&lt;/a&gt;.&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7jbbkknr7w0175cswdg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7jbbkknr7w0175cswdg.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Declare the headers and create an promise image upload function to call &lt;a href="https://apidocs.imgur.com/#c85c9dfc-7487-4de2-9ecd-66f727cf3139" rel="noopener noreferrer"&gt;imgur API&lt;/a&gt;(&lt;a href="https://api.imgur.com/3/image" rel="noopener noreferrer"&gt;https://api.imgur.com/3/image&lt;/a&gt;). (request.js in &lt;a href="https://github.com/medusajs/admin/blob/master/src/services/request.js" rel="noopener noreferrer"&gt;src/services&lt;/a&gt;)&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const headers = {
  "Content-Type": "multipart/form-data",
  Authorization: "Client-ID " + IMGUR_CLIENT_ID,
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const imgurUploadPromise = (formData) =&amp;gt; {
  const apiUrl = "https://api.imgur.com/3/image"
  return axios
    .post(apiUrl, formData, {
      headers,
    })
    .then((res) =&amp;gt; {
      console.log(res)
      return res.data.data.link
    })
    .catch((err) =&amp;gt; console.log(err))
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure the link of response format should be &lt;em&gt;res.data.data.link&lt;/em&gt;. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using promise.all to upload multiple images in one operation. (api.js in &lt;a href="https://github.com/medusajs/admin/blob/master/src/services/api.js" rel="noopener noreferrer"&gt;src/services&lt;/a&gt;)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  uploads: {
    create(files) {
      const promiseArray = []
      for (const f of files) {
        const formData = new FormData()
        formData.append("image", f)
        promiseArray.push(imgurUploadPromise(formData))
      }

      return Promise.all(promiseArray).then((values) =&amp;gt; {
        const uploads = values.map((value) =&amp;gt; {
          return { url: value }
        })
        return {
          data: {
            uploads,
          },
        }
      })
    },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember the return data format must be &lt;em&gt;{ url: value }&lt;/em&gt; for each promise and return &lt;em&gt;data: { uploads }&lt;/em&gt; for all.&lt;/p&gt;

&lt;p&gt;Then it will be done successfully. (don't upload images each over than &lt;a href="https://help.imgur.com/hc/en-us/articles/115000083326-What-files-can-I-upload-Is-there-a-size-limit-#:~:text=The%20maximum%20file%20size%20for,will%20be%20converted%20to%20JPEGs.&amp;amp;text=Non%2Danimated%20images%20over%201MB,holders%20will%20be%20lossily%20compressed." rel="noopener noreferrer"&gt;5MB&lt;/a&gt;)&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqmim6zjthhj7pu9z1z8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqmim6zjthhj7pu9z1z8.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>medusa</category>
      <category>imgur</category>
      <category>promise</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
