<?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: Karolis</title>
    <description>The latest articles on Forem by Karolis (@krusenas).</description>
    <link>https://forem.com/krusenas</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%2F111664%2Fdebc8430-a473-4906-9f71-12db0d030885.jpg</url>
      <title>Forem: Karolis</title>
      <link>https://forem.com/krusenas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/krusenas"/>
    <language>en</language>
    <item>
      <title>How to setup on-prem Jenkins with Bitbucket</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Wed, 27 Sep 2023 22:37:27 +0000</pubDate>
      <link>https://forem.com/krusenas/how-to-setup-on-prem-jenkins-with-bitbucket-4geo</link>
      <guid>https://forem.com/krusenas/how-to-setup-on-prem-jenkins-with-bitbucket-4geo</guid>
      <description>&lt;p&gt;In this tutorial, we will show a Jenkins Bitbucket integration using webhooks. It will work behind a firewall, inside a private network. You can use this setup for other services too - such as GitHub, GitLab or anything else that emits webhooks.&lt;/p&gt;

&lt;p&gt;Main advantages of Webhook Relay here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No delay between polling requests&lt;/li&gt;
&lt;li&gt;Additional security layer as Jenkins is not exposed to the internet as webhooks by default are uni-directional, responses are not returned to the caller.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjx6t5enu602jf83yjg27.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%2Fjx6t5enu602jf83yjg27.png" alt="Github to Jenkins without public IP"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Webhook Relay account, create one &lt;a href="https://my.webhookrelay.com" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;Jenkins instance. We will not go into installing Jenkins itself as there are quite a few options and many articles on that. See Jenkins &lt;a href="https://www.jenkins.io/doc/book/installing/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; for up-to-date instructions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bitbucket.com" rel="noopener noreferrer"&gt;Bitbucket&lt;/a&gt; account and a repository that you will want to use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configure webhook forwarding
&lt;/h2&gt;

&lt;p&gt;We will be using the &lt;a href="https://plugins.jenkins.io/bitbucket/" rel="noopener noreferrer"&gt;Jenkins Bitbucket plugin&lt;/a&gt;  . This plugin exposes a single endpoint to which we can send webhooks from multiple Bitbucket repositories.&lt;/p&gt;

&lt;p&gt;Go to the internal URL forwarding setup page &lt;a href="https://my.webhookrelay.com/new-internal-destination" rel="noopener noreferrer"&gt;https://my.webhookrelay.com/new-internal-destination&lt;/a&gt; and enter your Jenkins address. In my case I will be running the agent on the same machine as Jenkins so the address for me is &lt;code&gt;http://localhost:8080/bitbucket-hook/&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqay6q0kcs5kluln7v5a.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%2Foqay6q0kcs5kluln7v5a.png" alt="Webhook Relay internal forwarding configuration helper"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Follow the instructions to setup the agent and being forwarding webhooks. You will get your public URL that you can use in Bitbucket webhook configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzrfznopf4yybqj2qx76p.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%2Fzrfznopf4yybqj2qx76p.png" alt="Our public URL is 'https://aqjftr6vxxtfrjfrvcqrku.hooks.webhookrelay.com'"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see in the screenshot above, take the "Listening on" address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Bitbucket
&lt;/h2&gt;

&lt;p&gt;For Bitbucket webhook configuration you can follow the plugin guide here: &lt;a href="https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-cloud-usage" rel="noopener noreferrer"&gt;https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-cloud-usage&lt;/a&gt;. You need to go to the repository settings and then to the webhooks section add "Add webhook" with the public URL that you have gotten from the previous step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftm9ys9nsetk3l6bexfd3.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%2Ftm9ys9nsetk3l6bexfd3.png" alt="Bitbucket webhook configuration section"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bitbucket will be sending webhooks to Webhook Relay and our service will forwarding them to your internal Jenkins instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Jenkins
&lt;/h2&gt;

&lt;p&gt;Ensure you have the Bitbucket Jenkins plugin. Plugin instructions can be found here: &lt;a href="https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-cloud-usage" rel="noopener noreferrer"&gt;https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-cloud-usage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In your repository configure the build trigger:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjaku4sdst1xkg6z9ls1l.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%2Fjaku4sdst1xkg6z9ls1l.png" alt="configuring build trigger"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Once the agent is running, you can test by pushing a commit to your repository. You should see a build being triggered in Jenkins:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fin8fcoq8xn0q907h6uke.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%2Fin8fcoq8xn0q907h6uke.png" alt="push triggers the build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should also see it in the terminal where you started Webhook Relay agent:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

relay forward &lt;span class="nt"&gt;-b&lt;/span&gt; localhost-9Jk06s
Filtering on bucket: localhost-9Jk06s
Starting webhook relay agent...
2023-09-24 23:03:10.884 INFO    using standard transport...
2023-09-24 23:03:10.951 INFO    webhook relay ready...  &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"host"&lt;/span&gt;: &lt;span class="s2"&gt;"my.webhookrelay.com:8080"&lt;/span&gt;, &lt;span class="s2"&gt;"buckets"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"localhost-9Jk06s"&lt;/span&gt;&lt;span class="o"&gt;]}&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Troubleshooting
&lt;/h3&gt;

&lt;p&gt;There are several places to look for logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webhook Relay bucket details. It will show all your webhooks and their requests, responses.&lt;/li&gt;
&lt;li&gt;Jenkins system logs. You can find them in the Jenkins UI under "Manage Jenkins" -&amp;gt; "System Log". &lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Response "200" in Webhook Relay logs but no build
&lt;/h4&gt;

&lt;p&gt;It's possible that you don't have the SCM configuration matching your Bitbucket repository. Check the system logs in Jenkins for errors such as:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

PM WARNING com.cloudbees.jenkins.plugins.BitbucketJobProbe triggerMatchingJobs
No SCM configuration was found!


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

&lt;/div&gt;

&lt;p&gt;If you find them, add your repository to the SCM configuration in the Jenkins job.&lt;/p&gt;

&lt;h4&gt;
  
  
  On Webhook Relay all logs appear as "received"
&lt;/h4&gt;

&lt;p&gt;You need to start the agent, follow the instructions here &lt;a href="https://webhookrelay.com/v1/installation/" rel="noopener noreferrer"&gt;https://webhookrelay.com/v1/installation/&lt;/a&gt;. Agent is required to run in order to receive and forward webhooks.&lt;/p&gt;

&lt;h4&gt;
  
  
  Bitbucket server
&lt;/h4&gt;

&lt;p&gt;Bitbucket server webhook to jenkins example can be found here &lt;a href="https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-server-usage" rel="noopener noreferrer"&gt;https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-server-usage&lt;/a&gt;. You mostly need to install a webhook plugin &lt;a href="https://marketplace.atlassian.com/apps/1215474/post-webhooks-for-bitbucket?hosting=server&amp;amp;tab=overview" rel="noopener noreferrer"&gt;https://marketplace.atlassian.com/apps/1215474/post-webhooks-for-bitbucket?hosting=server&amp;amp;tab=overview&lt;/a&gt; and then create a Post-WebHook, which is different from WebHook and enable on push.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://webhookrelay.com/v1/tutorials/bitbucket/" rel="noopener noreferrer"&gt;https://webhookrelay.com/v1/tutorials/bitbucket/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>jenkins</category>
      <category>bitbucket</category>
      <category>networking</category>
      <category>cicd</category>
    </item>
    <item>
      <title>How good are Docker containers in the IoT devices?</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Tue, 01 Feb 2022 10:50:44 +0000</pubDate>
      <link>https://forem.com/krusenas/how-good-are-docker-containers-in-the-iot-devices-3o2b</link>
      <guid>https://forem.com/krusenas/how-good-are-docker-containers-in-the-iot-devices-3o2b</guid>
      <description>&lt;p&gt;Docker containers (also known as &lt;a href="https://opencontainers.org/"&gt;OCI&lt;/a&gt; containers) are now the most popular way to build and ship applications, whether it's through plain Docker, Docker-Compose, Kubernetes, or hosted services like Heroku, GCP CloudRun. The ease of building and deploying containers made it a no-brainer for developers and ops teams to adopt the new technology.&lt;/p&gt;

&lt;p&gt;But what about IoT devices? In this post, we will look into what's good about Docker containers on the edge and things to watch out for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are Docker containers ready for the IoT devices?
&lt;/h3&gt;

&lt;p&gt;Yes. The devices themselves are now plenty powerful to run a regular Linux distro, have a Docker engine installed, and run your containers without much overhead. Most of the devices out there, even with dated hardware are often more powerful than the small VMs provided by major cloud services.&lt;/p&gt;

&lt;p&gt;Synpse has been deployed on large fleets of devices that were comparable to RaspberryPi 2 (much weaker than the current generation) and we haven't observed any performance-related issues so far. &lt;/p&gt;

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

&lt;p&gt;For example, looking at &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/"&gt;Pi Zero 2&lt;/a&gt; which is priced as low as $15 can offer you a quad-core 64-bit ARM Cortex-A53 processor clocked at 1GHz and 512MB of SDRAM. Or we can look at the regular &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/"&gt;Raspberry Pi 4&lt;/a&gt; or &lt;a href="https://developer.nvidia.com/embedded/jetson-agx-xavier-developer-kit"&gt;Jetson Xavier&lt;/a&gt; which can provide a much higher amount of computing power.&lt;/p&gt;

&lt;h3&gt;
  
  
  Software deployment and OTA updates
&lt;/h3&gt;

&lt;p&gt;The biggest advantage of Docker is the ability to package the application the way you want and not worry about the host machine too much (you still need to think about the OS but a lot less). Before writing software in Go I used to write backend applications in Python. Application deployment used to be complicated, updates brittle and once in a while we would get some dependency clash. Docker solved this by packaging the whole filesystem with installed dependencies. You have your packages, your static files placed in the container.&lt;/p&gt;

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

&lt;p&gt;This way you can build the container, run automated tests against it and then deploy it to the servers. It also paves way for a simple rollback mechanism, where if things go wrong, you can just start the older version of the Docker container.&lt;/p&gt;

&lt;p&gt;For IoT products with tiny applications and fewer packages, the OTA update process is usually lightweight and simple: a new image is downloaded, the running container is stopped, a new container from the new Docker image is started.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparing multi-arch (or at least arm) images
&lt;/h3&gt;

&lt;p&gt;The main difference between x86/amd64 Docker images and the ones that you will probably need to deploy to the IoT devices is their architecture. This can sometimes be a gotcha when trying to run your containers on a RaspberryPi or some other device as the errors are often cryptic. &lt;/p&gt;

&lt;p&gt;Thankfully, DockerHub and other registries support multi-arch images where you can push both x86, arm 32-bit, and arm 64-bit images. This greatly simplifies deployment as the Docker daemon will figure out on its own, which image to download. We have written an article on &lt;a href="https://synpse.net/blog/images/multiarch-images/"&gt;building multi-arch images&lt;/a&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Persisting data
&lt;/h3&gt;

&lt;p&gt;An application that performs AI tasks such as image processing or has local databases need to persist data. When people just start using Docker containers, storage can be confusing as anything saved inside the container is not persisted when a container is recreated during the update. The way to persist data is to mount a volume from the device's filesystem into the container. You can read more about persistence in Docker &lt;a href="https://docs.docker.com/storage/volumes/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write/read-heavy directories and extending device's filesystem lifetime
&lt;/h3&gt;

&lt;p&gt;When preparing a device for an extended time in service, you need to think not only about the updates but also about the longevity of its components. SD cards or inbuilt storage can last for a long time but there are ways to reduce the load on them.&lt;/p&gt;

&lt;p&gt;If your application needs to write and read large amounts of data, it's often good to also mount &lt;code&gt;/dev/shm&lt;/code&gt;. You can read more about shared memory &lt;a href="https://en.wikipedia.org/wiki/Shared_memory"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exposing hardware devices into the containers
&lt;/h3&gt;

&lt;p&gt;Quite often you will need to use serial devices. Containerized applications can access them without any problems but you need to let Docker know that you want to expose these devices to your containers. On the Docker CLI it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/ttyACM0 ubuntu bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, in Synpse you can add the following into your application spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;devices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/ttyACM0&lt;/span&gt;
        &lt;span class="na"&gt;containerPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/ttyACM0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Security
&lt;/h3&gt;

&lt;p&gt;When deploying software to thousands of devices you need to think about "day two" operations. Even before releasing your application, it's crucial to plan and test both OS and application updates. How will you update the application? If the device is connected to the internet, what are the best methods to keep it secure even years later?&lt;/p&gt;

&lt;p&gt;By reducing the number of packages needed to be installed on the device, you are also drastically reducing the attack surface. For example, if you are deploying NodeJS, Python, Ruby based applications, your device OS doesn't need to have the runtime and can be as bare-bones as possible. This helps a lot, whatever is not installed, doesn't need to be updated.&lt;/p&gt;

&lt;p&gt;You can also use tools to automatically scan your images for vulnerabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/marketplace/actions/container-image-scan"&gt;https://github.com/marketplace/actions/container-image-scan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/docker-hub/vulnerability-scanning/"&gt;https://docs.docker.com/docker-hub/vulnerability-scanning/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Developer productivity
&lt;/h3&gt;

&lt;p&gt;Another important aspect when deploying containerized applications to the edge is reusing the tools that are already well known to your developers and ops teams. By choosing Docker for the edge, you aren't introducing a new packaging system. You can also reuse your CI pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summing up
&lt;/h3&gt;

&lt;p&gt;If your hardware and OS allow, it's almost always better to choose Docker containers for deploying your applications. It will increase security, simplify the deployment process and reduce the overall amount of issues over a long time of operation. The important part becomes a solid strategy to update your containers.&lt;/p&gt;

&lt;h3&gt;
  
  
  OTA updates with Synpse
&lt;/h3&gt;

&lt;p&gt;When it comes to managing large fleets of containers - Synpse was built for this. It provides everything you might need to successfully deploy and run a large scale operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declarative application deployment, store configuration in GitHub or any other source control management system&lt;/li&gt;
&lt;li&gt;Simple rollbacks&lt;/li&gt;
&lt;li&gt;GPU support&lt;/li&gt;
&lt;li&gt;Volume support&lt;/li&gt;
&lt;li&gt;SSH support with &lt;a href="https://www.ansible.com/"&gt;Ansible&lt;/a&gt; integration for OS updates&lt;/li&gt;
&lt;li&gt;Application logs for each device&lt;/li&gt;
&lt;li&gt;Metrics collection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can try out Synpse for free (up to 5 devices). Check out our &lt;a href="https://docs.synpse.net/start-here/quick-start-web-user"&gt;quick start&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>iot</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Putting your RaspberryPi to work with a sleek torrent client &amp; Plex TV setup</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Wed, 29 Dec 2021 13:18:03 +0000</pubDate>
      <link>https://forem.com/krusenas/putting-your-raspberrypi-to-work-with-a-sleek-torrent-client-plex-tv-setup-2akl</link>
      <guid>https://forem.com/krusenas/putting-your-raspberrypi-to-work-with-a-sleek-torrent-client-plex-tv-setup-2akl</guid>
      <description>&lt;h3&gt;
  
  
  Intro
&lt;/h3&gt;

&lt;p&gt;I guess we all know that torrents present a fantastic opportunity (amongst other things) for families to digitize their old VHS tapes that contain embarrassing videos of your childhood and then share these videos with others using an efficient BitTorrent protocol. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfmzsis5suih3o8c3buz.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%2Fkfmzsis5suih3o8c3buz.png" alt="Image description"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Then, once everyone has those videos that were supposed to die together with the old technology we also need to watch them, this is where the good guy Plex comes in.&lt;/p&gt;

&lt;p&gt;In this short tutorial we will setup a Vuetorrent and Plex combo on a RaspberryPi. Using this installation method you will get few great things out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag &amp;amp; drop torrent files to download them. You can select individual files from a larger torrent like in any other client.&lt;/li&gt;
&lt;li&gt;Shared directory between torrent client container and Plex so you can view them&lt;/li&gt;
&lt;li&gt;Optional public HTTPS URL to connect to Vuetorrent from anywhere in a form &lt;strong&gt;&lt;a href="https://dev-%7BYOUR" rel="noopener noreferrer"&gt;https://dev-{YOUR&lt;/a&gt; ID}.synpse.cloud&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hardware
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Raspberry Pi 4&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.okdo.com/p/okdo-multihead-pi-4-power-supply-5-1v-3a/" rel="noopener noreferrer"&gt;5V 3A power supply&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.co.uk/gp/product/B07NY23WBG/ref=ppx_yo_dt_b_asin_title_o08_s00" rel="noopener noreferrer"&gt;128GB SD card&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Software
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cloud.synpse.net" rel="noopener noreferrer"&gt;Synpse&lt;/a&gt; - provides app deployment, SSH and HTTPS access.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/WDaan/VueTorrent" rel="noopener noreferrer"&gt;Vuetorrent&lt;/a&gt; - Vuetorrent is a nice looking skin for otherwise a bit dated &lt;a href="https://www.qbittorrent.org/" rel="noopener noreferrer"&gt;qbittorrent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.plex.tv/en-gb/" rel="noopener noreferrer"&gt;Plex&lt;/a&gt; - probably the best option to stream things locally to your smart TV. &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Install Docker
&lt;/h3&gt;

&lt;p&gt;Depending on your RaspberryPi OS Docker installation might vary but in most cases this script from &lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;https://docs.docker.com/engine/install/ubuntu&lt;/a&gt; should do the job: &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Step 2: Install Synpse agent on your device
&lt;/h3&gt;

&lt;p&gt;Log into your &lt;a href="https://cloud.synpse.net/" rel="noopener noreferrer"&gt;Synpse account&lt;/a&gt;, navigate to the "Devices" section and then click on "Provision". Use the command in your RaspberryPi terminal. This will download, install and initialize the synpse agent.&lt;/p&gt;

&lt;p&gt;Once the device has appeared in your Synpse dashboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjupxyd5sx7gay5x584vg.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%2Fjupxyd5sx7gay5x584vg.png" alt="RaspberryPi"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Once device is visible, click on the "edit labels" from the menu. Add a label 'type: rpi':&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwny82taaflm99lhp90u.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%2Fkwny82taaflm99lhp90u.png" alt="Label your device"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Synpse, applications are installed into the devices based on device labels so it's a good practice to label your devices accordingly.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Create Vuetorrent and Plex deployment
&lt;/h3&gt;

&lt;p&gt;Applications in Synpse are defined in yaml format. If you have used Docker Compose or Kubernetes before, it will feel familiar. You can read more about them &lt;a href="https://docs.synpse.net/synpse-core/applications" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;To create the application:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the namespace (should be 'default' on the left side menu)&lt;/li&gt;
&lt;li&gt;Click on "New Application" button in the top&lt;/li&gt;
&lt;li&gt;Copy paste the yaml from below&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="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;torrent&lt;/span&gt;
&lt;span class="na"&gt;scheduling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Conditional&lt;/span&gt;
  &lt;span class="na"&gt;selectors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rpi&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&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;qbittorrent&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cr.hotio.dev/hotio/qbittorrent&lt;/span&gt; &lt;span class="c1"&gt;# Using an image that already contains vuetorrent&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;80:8080&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/data/qbittorrent/config:/config&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/media/downloads:/downloads&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./vuetorrent:/vuetorrent&lt;/span&gt;
      &lt;span class="na"&gt;env&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;PUID&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000"&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;PGID&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000"&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;TZ&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Europe/London&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;UMASK_SET&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;022"&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;WEBUI_PORT&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080"&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;plex&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lscr.io/linuxserver/plex&lt;/span&gt;
      &lt;span class="na"&gt;networkMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt; &lt;span class="c1"&gt;# Using host network as it will be exposing quite a few ports for various protocols&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/data/plex:/config&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/media/downloads:/media/downloads&lt;/span&gt;
      &lt;span class="na"&gt;env&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;PUID&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000"&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;PGID&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000"&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;VERSION&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&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;PLEX_CLAIM&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# Optional, you can get your claim here https://www.plex.tv/claim/&lt;/span&gt;



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

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Logging into the Vuetorrent:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once deployed, it will take a few seconds or minutes (depending on your internet speed) to download and start the docker containers. Once started, you can access it on &lt;em&gt;http://{YOUR DEVICE IP}:80&lt;/em&gt; address. Initial username is &lt;code&gt;admin&lt;/code&gt; and password is &lt;code&gt;adminadmin&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Once logged in, go into settings and configure the UI theme:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fod05zuedber8yy7rleoy.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%2Fod05zuedber8yy7rleoy.png" alt="update qbittorrent theme"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, change your password:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvrimbhiohi5c6mpj96b.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%2Fpvrimbhiohi5c6mpj96b.png" alt="update vuetorrent password"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging into Plex:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can login into it using port &lt;em&gt;http://{YOUR DEVICE IP}:32400&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Configuring Plex libraries
&lt;/h3&gt;

&lt;p&gt;To see whatever you have downloaded via Vuetorrent to your local machine in Plex. For both Films and TV Shows the media path should be &lt;code&gt;/media/downloads&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzoo1xpc1wz0o5drrv2ka.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%2Fzoo1xpc1wz0o5drrv2ka.png" alt="configuring library"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 (optional): Expose it to the internet so you can connect from anywhere
&lt;/h3&gt;

&lt;p&gt;At this step we will enable remote access via HTTPS to our Vuetorrent installation on the RaspberryPi. If you have skipped password setup, please do it, otherwise malicious users or automated bots could hijack your device.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your device details&lt;/li&gt;
&lt;li&gt;In the top right corner set the port to your Vuetorrent port which should be 80&lt;/li&gt;
&lt;li&gt;Turn on the switch&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Connect your smart TV
&lt;/h3&gt;

&lt;p&gt;Most TVs now have inbuilt Plex TV apps. You can use it to pair it with your Raspberry Pi server. As long as you are using same account, it should just work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Download content
&lt;/h3&gt;

&lt;p&gt;Add a torrent to the Vuetorrent dashboard. Once downloaded, it should appear in your Plex library.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.qbittorrent.org/" rel="noopener noreferrer"&gt;https://www.qbittorrent.org/&lt;/a&gt; - torrent client&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hotio.dev/containers/qbittorrent/" rel="noopener noreferrer"&gt;https://hotio.dev/containers/qbittorrent/&lt;/a&gt; - Docker image with embedded Vuetorrent&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hub.docker.com/r/linuxserver/plex/tags" rel="noopener noreferrer"&gt;https://hub.docker.com/r/linuxserver/plex/tags&lt;/a&gt; - LinuxServer.io provided Plex Docker image&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.plex.tv/en-gb/" rel="noopener noreferrer"&gt;https://www.plex.tv/en-gb/&lt;/a&gt; - streaming app&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://synpse.net/blog/vuetorrent-plex/" rel="noopener noreferrer"&gt;https://synpse.net/blog/vuetorrent-plex/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>tutorial</category>
      <category>beginners</category>
      <category>iot</category>
    </item>
    <item>
      <title>Self-hosted business intelligence with Metabase</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Thu, 11 Nov 2021 19:38:24 +0000</pubDate>
      <link>https://forem.com/krusenas/self-hosted-business-intelligence-with-metabase-3php</link>
      <guid>https://forem.com/krusenas/self-hosted-business-intelligence-with-metabase-3php</guid>
      <description>&lt;p&gt;It is always useful to know how your business or projects are doing and for that, there are a bunch of tools available such as Excel spreadsheets, Google DataStudio, Apache Superset, etc. I personally am a fan of &lt;a href="https://www.metabase.com/"&gt;Metabase&lt;/a&gt; as it is the easiest to deploy and use. When paired with the right technologies, this setup becomes trivial to any small or large organizations.&lt;/p&gt;

&lt;p&gt;Main benefits of self-hosting &lt;a href="https://www.metabase.com/"&gt;Metabase&lt;/a&gt; (other tools as well):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance - compared to Cloud VMs your own hardware will not be throttled even when it runs heavy queries.&lt;/li&gt;
&lt;li&gt;Costs - hardware like Intel NUC (or new ones from AMD) pay for themselves not in years but in months. I have mine for more than 2 years and it's been fantastic little helper.&lt;/li&gt;
&lt;li&gt;Privacy - since you need to supply connection strings to your detabase, it's much better to do it on your own hardware where you control the risks.&lt;/li&gt;
&lt;li&gt;Stability - managed instances can be updated at any time and can break your setup. This happened to us :)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we will use a setup that works exactly the same way on both an Intel NUC (for some of my projects) and on a large VM that is managed by a VMware. &lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://webhookrelay.com"&gt;Webhook Relay account&lt;/a&gt; - will be used to expose the Metabase to the internet, so we can access it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://synpse.net"&gt;Synpse account&lt;/a&gt; - a lightweight and fantastic platform to manage and run software on your own hardware. Offers management of up to 5 devices free of charge.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Install Synpse into your server/machine
&lt;/h2&gt;

&lt;p&gt;Once you log into &lt;a href="https://cloud.synpse.net"&gt;Synpse Cloud&lt;/a&gt;, select your project and then head to the "Devices" page. From there you will be able to find the auto-generated command that you need to run on the device to add it to your project. &lt;/p&gt;

&lt;p&gt;There are multiple ways to do it however initially you can just SSH into the machine via local network. Once you run the command, after a few seconds (depends on your internet speed) you should see the magic happen and device appear in your "Devices" page in Synpse :) &lt;/p&gt;

&lt;h2&gt;
  
  
  2. Create a Webhook Relay tunnel for your Metabase app
&lt;/h2&gt;

&lt;p&gt;Our Metabase deployment will need to be reachable from outside so we can actually view reports. For this, we are creating a Webhook Relay tunnel that will be established by a &lt;code&gt;webhookrelayd&lt;/code&gt; container.&lt;/p&gt;

&lt;p&gt;Go to your &lt;a href="https://my.webhookrelay.com/tunnels"&gt;https://my.webhookrelay.com/tunnels&lt;/a&gt; page and create a new tunnel with these details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name: 'whr-metabase' (&lt;code&gt;webhookrelayd&lt;/code&gt; agent will need to know which tunnel to use)&lt;/li&gt;
&lt;li&gt;destination: '&lt;a href="http://metabase:3000"&gt;http://metabase:3000&lt;/a&gt;' (metabase is reachable using container's name)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Get your access token key and secret
&lt;/h2&gt;

&lt;p&gt;Head to the tokens page here &lt;a href="https://my.webhookrelay.com/tokens"&gt;https://my.webhookrelay.com/tokens&lt;/a&gt; and create a new pair. Save the key and secret before closing the window.&lt;/p&gt;

&lt;p&gt;Go to the secrets page in Synpse and create both &lt;code&gt;relayKey&lt;/code&gt; and &lt;code&gt;relaySecret&lt;/code&gt; secrets:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  4. Deploy Metabase via Synpse
&lt;/h2&gt;

&lt;p&gt;Last step is to create a Synpse application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metabase&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metabase + WHR&lt;/span&gt;
&lt;span class="na"&gt;scheduling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Conditional&lt;/span&gt;
  &lt;span class="na"&gt;selectors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;controller&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&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;metabase&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metabase/metabase:latest&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/data/metabase:/metabase-data&lt;/span&gt;
      &lt;span class="na"&gt;env&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;MB_DB_FILE&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/metabase-data/metabase.db&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;MB_REDIRECT_ALL_REQUESTS_TO_HTTPS&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&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;relayd&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webhookrelay/webhookrelayd:1&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--mode&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tunnel&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-t&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whr-metabase&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;--- if you have chosen a different name for the tunnel, change it here too&lt;/span&gt;
      &lt;span class="na"&gt;env&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;RELAY_KEY&lt;/span&gt;
          &lt;span class="na"&gt;fromSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;relayKey&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;RELAY_SECRET&lt;/span&gt;
          &lt;span class="na"&gt;fromSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;relaySecret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once deployed, use your tunnel URL to access it. You can also configure Google OAuth to make the login easier, however that's out of scope in this article.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://webhookrelay.com/blog/2021/11/11/setting-up-selfhosted-metabase/"&gt;https://webhookrelay.com/blog/2021/11/11/setting-up-selfhosted-metabase/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>showdev</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Manage your meetings like a boss with self-hosted calender</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Sat, 02 Oct 2021 21:12:12 +0000</pubDate>
      <link>https://forem.com/krusenas/manage-your-meetings-like-a-boss-with-self-hosted-calender-4hlb</link>
      <guid>https://forem.com/krusenas/manage-your-meetings-like-a-boss-with-self-hosted-calender-4hlb</guid>
      <description>&lt;p&gt;Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, side projects and anything else where we need some flexible coordination. However, most tools are very limited in terms of control, customization, data protection, and most important - they are not free! That's where Calendso &amp;amp; Synpse come in. Self-hosted, all your data sitting on your own machines. API-driven and ready to be deployed on your own domain. Full control of your events and data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In this tutorial we will be deploying a self-hosted Calendso instance that lives under your own DNS domain. You will be able to share meeting links such as &lt;a href="https://calendar.synpse.net/karolis/quick-chat" rel="noopener noreferrer"&gt;https://calendar.synpse.net/karolis/quick-chat&lt;/a&gt; with your colleagues, clients and friends:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg6myuf6j24ngkxg7qv7z.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%2Fg6myuf6j24ngkxg7qv7z.png" alt="calendso-karolis"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will also be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integrate it with your Google calendar &lt;/li&gt;
&lt;li&gt;Create multiple users for your organization&lt;/li&gt;
&lt;li&gt;Create teams&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Deployment will be handled in a declarative way through Synpse. Incoming traffic will be handled by &lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt; server which will terminate HTTPS (it will also automatically retrieve TLS certificates for you) and route traffic to &lt;a href="https://cal.com/" rel="noopener noreferrer"&gt;Calendso&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You will also be able to access &lt;a href="https://www.prisma.io/studio" rel="noopener noreferrer"&gt;Prisma Studio&lt;/a&gt; locally to create your and your team's users.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cloud.synpse.net" rel="noopener noreferrer"&gt;Synpse account&lt;/a&gt; - Deployment&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloudflare.com" rel="noopener noreferrer"&gt;Cloudflare account&lt;/a&gt; - DNS (or any other DNS provider)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't have your own DNS domain, just jump in our Discord channel and I will help you out with some alternatives (you can either use Webhook Relay's subdomains or use services like DuckDNS).&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure DNS
&lt;/h2&gt;

&lt;p&gt;If you are using Cloudflare, creating an A record looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxq5di9bxi85vrb3e43h.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%2Fqxq5di9bxi85vrb3e43h.png" alt="a-record"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can consult your DNS provider's documentation on managing A records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure port forwarding on your router (optional)
&lt;/h2&gt;

&lt;p&gt;If you server where you are setting this up is within your local network, you will need to expose it to the internet. The easy way to do it is just using your own router to forward the ports:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F19mbva0jr22ss7wope8b.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%2F19mbva0jr22ss7wope8b.png" alt="port-forward"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you don't have a static IP or your network is behind a CGNAT, you can use &lt;a href="https://webhookrelay.com" rel="noopener noreferrer"&gt;https://webhookrelay.com&lt;/a&gt; instead. We will be providing a tutorial on that setup as well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Integrating with Google calendar
&lt;/h2&gt;

&lt;p&gt;Bookings are great but it would be tedious to check your Calendso instance every day. To allow Calendso to read from and write to our Google calendar, we need to create API credentials and provide them to Calendso. &lt;/p&gt;

&lt;h3&gt;
  
  
  Enable the API
&lt;/h3&gt;

&lt;p&gt;To do that, head to the Google API console and create a new project. It might take a little bit for Google to set up your project but once that's done, head over to the project and click the Enable APIs and Services button.&lt;/p&gt;

&lt;p&gt;Once you click the button, you'll be taken to the API library. In the search box, look for calendar and select the Google Calendar API result and then enable it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieve credentials
&lt;/h3&gt;

&lt;p&gt;Next, let's head to our credentials page in the Google API console so we can create the Calendso OAuth ID and add our production origin and redirect URI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jmjgjwhet5u7f1t3aoz.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%2F7jmjgjwhet5u7f1t3aoz.png" alt="gcp-creds"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your key will be created and you'll be redirected back to the Credentials page where you will see your newly generated OAuth 2.0 Client ID. Select the download option and save this file to your computer. Copy all the contents of the file and add that as the value for the GOOGLE_API_CREDENTIALS environment variable:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9nnbov8g9lkka5xa649y.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%2F9nnbov8g9lkka5xa649y.png" alt="gcp-creds-secret"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy through Synpse
&lt;/h2&gt;

&lt;p&gt;With Synpse we will be deploying multiple containers to provide us with admin capabilities, database, tunneling (to expose it to the internet) and the Calendso itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparing the secrets
&lt;/h3&gt;

&lt;p&gt;First, go to your secrets page and create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;calendsoPostgres&lt;/strong&gt; with your database password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;calendsoPostgresConnString&lt;/strong&gt; with &lt;code&gt;postgresql://calendso:PASSWORD@postgres:5432/calendso&lt;/code&gt; (just replace PASSWORD with your database password)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;calendsoEncKey&lt;/strong&gt; with a random key that you generate yourself. You can generate one in your terminal with command &lt;code&gt;openssl rand -base64 32&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipy3qvmnyw8iyjemtnf7.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%2Fipy3qvmnyw8iyjemtnf7.png" alt="secrets"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the application
&lt;/h3&gt;

&lt;p&gt;The full spec looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Name of your app&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;synpse-calendso&lt;/span&gt;
&lt;span class="na"&gt;scheduling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Conditional&lt;/span&gt;
  &lt;span class="c1"&gt;# Need to match your labels&lt;/span&gt;
  &lt;span class="c1"&gt;# Ref: https://docs.synpse.net/synpse-core/applications/scheduling&lt;/span&gt;
  &lt;span class="na"&gt;selectors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;controller&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&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;calendso&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ctadeu/calendso:0.0.17-1&lt;/span&gt;
      &lt;span class="na"&gt;env&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;BASE_URL&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://calendar.synpse.net&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;NEXTAUTH_URL&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://calendar.synpse.net&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;DATABASE_URL&lt;/span&gt;
          &lt;span class="na"&gt;fromSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;calendsoPostgresConnString&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;CALENDSO_ENCRYPTION_KEY&lt;/span&gt;
          &lt;span class="na"&gt;fromSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;calendsoEncKey&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;GOOGLE_API_CREDENTIALS&lt;/span&gt;
          &lt;span class="na"&gt;fromSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;calendsoGoogleApiCredentials&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&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;prisma&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;codejamninja/prisma-studio:latest&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5555:5555&lt;/span&gt;
      &lt;span class="na"&gt;env&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;POSTGRES_URL&lt;/span&gt;
          &lt;span class="na"&gt;fromSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;calendsoPostgresConnString&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&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;postgres&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:latest&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/data/calendso-postgres:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="na"&gt;env&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;PGDATA&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/postgresql/data/pgdata&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;POSTGRES_USER&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;calendso&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;POSTGRES_DB&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;calendso&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;POSTGRES_PASSWORD&lt;/span&gt;
          &lt;span class="na"&gt;fromSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;calendsoPostgres&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
    &lt;span class="c1"&gt;# Caddy provides HTTPS&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;caddy&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:latest&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;reverse-proxy&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--from&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;calendar.synpse.net:7300&lt;/span&gt; &lt;span class="c1"&gt;# This should be your domain and port, same as in your router&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--to&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;calendso:3000&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;7300:7300&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/data/calendso-caddy:/data&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/data/calendso-caddy-cfg:/config&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now, if you click on Allocations, you should see all containers running and in device details you can see the ports:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5goljuhlg5vkjaztjuxn.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%2F5goljuhlg5vkjaztjuxn.png" alt="synpse-device"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you click on Prisma and if your device is locally reachable, you will be able to access the admin dashboard.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note, that if your device is exposed to the internet, you should not leave the Prisma container with the port exposed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once in Prisma, follow the official guide from &lt;a href="https://docs.cal.com/docs/self-hosting/docker" rel="noopener noreferrer"&gt;self-hosting section&lt;/a&gt; and:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on the User model to add a new user record.&lt;/li&gt;
&lt;li&gt;Fill out the fields (remembering to encrypt your password with &lt;a href="https://bcrypt-generator.com/" rel="noopener noreferrer"&gt;BCrypt&lt;/a&gt;) and click Save 1 Record to create your first user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you have logged in, go to the integrations section and connect it with the Google's calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Calendso docs: &lt;a href="https://docs.cal.com/" rel="noopener noreferrer"&gt;https://docs.cal.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Synpse docs: &lt;a href="https://docs.synpse.net" rel="noopener noreferrer"&gt;https://docs.synpse.net&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Webhook Relay docs: &lt;a href="https://docs.webhookrelay.com" rel="noopener noreferrer"&gt;https://docs.webhookrelay.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloudflare: &lt;a href="https://cloudflare.com" rel="noopener noreferrer"&gt;https://cloudflare.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://synpse.net/blog/self-hosting-calendso-caddy/" rel="noopener noreferrer"&gt;https://synpse.net/blog/self-hosting-calendso-caddy/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Showdev: deploy apps to edge devices and servers with Synpse</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Thu, 02 Sep 2021 14:07:39 +0000</pubDate>
      <link>https://forem.com/krusenas/showdev-deploy-apps-to-edge-devices-and-servers-with-synpse-43k4</link>
      <guid>https://forem.com/krusenas/showdev-deploy-apps-to-edge-devices-and-servers-with-synpse-43k4</guid>
      <description>&lt;p&gt;Hey dev.to! I wanted to share our project &lt;a href="https://synpse.net" rel="noopener noreferrer"&gt;Synpse&lt;/a&gt; that my friend and I have been working on for quite some time. &lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Registration is now open at &lt;a href="https://cloud.synpse.net" rel="noopener noreferrer"&gt;https://cloud.synpse.net&lt;/a&gt;. Free for up to 5 devices forever. We can increase your quotas if requested. Self-hosted version available as well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Synpse provides tooling and services to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prepare machine images that you can burn into SD cards to quickly bootstrap hundreds or thousands of devices.&lt;/li&gt;
&lt;li&gt;Generated simple installation script for one-off device/server bootstrap.&lt;/li&gt;
&lt;li&gt;Group devices based on labels (used for both filtering and scheduling applications).&lt;/li&gt;
&lt;li&gt;Automated device naming (based on hostname, random names, etc.).&lt;/li&gt;
&lt;li&gt;Deploy containerized applications (Docker Compose/K8s Pod style).&lt;/li&gt;
&lt;li&gt;Automated device software updates.&lt;/li&gt;
&lt;li&gt;Easily scale to tens or hundreds of thousands of devices.&lt;/li&gt;
&lt;li&gt;SSH into any of your devices through &lt;code&gt;synpse&lt;/code&gt; CLI or web UI.&lt;/li&gt;
&lt;li&gt;Kubectl style port-forward command to open up TCP tunnels.&lt;/li&gt;
&lt;li&gt;CPU/RAM utilization metrics from device.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;While it's easy to configure a systemd or some other init service in an OS image to launch your application on device boot - it won't help much for fast updates or gradual rollout. It will also not help much if during the rollout you need to collect some data from applications (logs, SSH access to check some files).&lt;/p&gt;

&lt;p&gt;We had a need to deploy quite complex piece of software to ~1200 devices. After looking into a bunch of alternatives like Mender, Balena, Deviceplane we came to a conclusion that it's either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can't use my own image (we had a manufacturer that could only use their own Linux distro, new custom OS was not an option).&lt;/li&gt;
&lt;li&gt;Self-hosting is too complicated.&lt;/li&gt;
&lt;li&gt;Not scaleable (started considerably slowing down with very few devices).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;What our testing gave us was the understanding that the ideal solution would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple self-hosting, a horizontally scaleable controller and a managed database (we chose Postgres) without disrupting your bank balance. &lt;/li&gt;
&lt;li&gt;Must support any Linux OS.&lt;/li&gt;
&lt;li&gt;Don't try to be compatible with neither Kubernetes or Docker Compose in the API/config manifests. These systems require different semantics so it's fine to be similar but 1:1 mapping will just damage the UX.&lt;/li&gt;
&lt;li&gt;SSH and application logs are a must if you have an outage and you need a fast way to debug.&lt;/li&gt;
&lt;li&gt;Everything belongs to a project. Project can have multiple users but the projects should be able to survive multiple generations of users as these systems will last longer than many individual employees.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The feel
&lt;/h2&gt;

&lt;p&gt;So, one-off device provisioning script + execution parameters can be found in either "devices" or "provisioning" pages:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flt11lafuq1tzght0o61e.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%2Flt11lafuq1tzght0o61e.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once devices are added, they appear in your dashboard for SSH access, monitoring:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcex7nhs50e23afnecwtx.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%2Fcex7nhs50e23afnecwtx.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can read more about device management here: &lt;a href="https://docs.synpse.net/synpse-core/devices" rel="noopener noreferrer"&gt;https://docs.synpse.net/synpse-core/devices&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can add various labels and then use them in the application deployment:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0fsg4c1ya8gossdogy40.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%2F0fsg4c1ya8gossdogy40.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;View application docs here: &lt;a href="https://docs.synpse.net/synpse-core/applications" rel="noopener noreferrer"&gt;https://docs.synpse.net/synpse-core/applications&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Most applications have some shared or private configuration, luckily Synpse provides secrets (similar to Kubernetes ones) where you can reference to them from your application spec:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7dmk4skzoreobgylahel.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%2F7dmk4skzoreobgylahel.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And when things go south, view your application logs on any of the devices:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flb4hzqlqqic8qs3o5bzg.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%2Flb4hzqlqqic8qs3o5bzg.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Use cases
&lt;/h2&gt;

&lt;p&gt;As mentioned in the first paragraph, we built Synpse for a medium size fleet of ~1200 IoT devices and a bunch of large servers as it appears that the UX can be much nicer than Docker Compose or Kubernetes. All of those devices are actually card readers in a large bus/trolleybus fleet in Lithuania :) &lt;/p&gt;

&lt;p&gt;We would recommend using Synpse for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Home lab deployments (up to 5 devices free forever).&lt;/li&gt;
&lt;li&gt;PoS deployments.&lt;/li&gt;
&lt;li&gt;Public transport fleets.&lt;/li&gt;
&lt;li&gt;ML on edge (packaging and running ML inference through Docker containers).&lt;/li&gt;
&lt;li&gt;Any kind of industrial applications where devices can be offline for a prolonged time. Synpse will work well without internet access.&lt;/li&gt;
&lt;li&gt;Normal VMs on cloud providers that don't provide good application deployment UX (most smaller ones like Vultr, DO, Packer, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;If you run software yourself or in a company, you should definitely check it out. &lt;/p&gt;

&lt;p&gt;Useful links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.synpse.net/" rel="noopener noreferrer"&gt;https://docs.synpse.net/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://synpse.net" rel="noopener noreferrer"&gt;https://synpse.net&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Our Discord channel: &lt;a href="https://discord.gg/dkgN4vVNdm" rel="noopener noreferrer"&gt;https://discord.gg/dkgN4vVNdm&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>showdev</category>
      <category>productivity</category>
      <category>tooling</category>
      <category>iot</category>
    </item>
    <item>
      <title>CDN types and how to set one up (Vue, React)</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Mon, 27 Jul 2020 16:09:41 +0000</pubDate>
      <link>https://forem.com/krusenas/cdn-types-and-how-to-set-one-up-vue-react-k1b</link>
      <guid>https://forem.com/krusenas/cdn-types-and-how-to-set-one-up-vue-react-k1b</guid>
      <description>&lt;p&gt;What is CDN? Cloudflare has a nice explanation here: &lt;a href="https://www.cloudflare.com/learning/cdn/what-is-a-cdn/" rel="noopener noreferrer"&gt;https://www.cloudflare.com/learning/cdn/what-is-a-cdn/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In short:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A content delivery network (CDN) refers to a geographically distributed group of servers that work together to provide fast delivery of Internet content.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Many of you know CDNs from using various 3rd party resources such as fonts, css, certain javascript libraries (for example Stripe library for secure payments).&lt;/p&gt;

&lt;p&gt;In this article we will have a quick look into several CDN types and potentials pros/cons that you might encounter when setting one up yourself.&lt;/p&gt;

&lt;p&gt;Cloudflare is one of the best CDNs out there and I am using it for various landing pages. It's a great DNS configuration service too that provides rich APIs. However, it's good to understand what other types of CDNs are out there and which suits you best.&lt;/p&gt;

&lt;h2&gt;
  
  
  CDN types
&lt;/h2&gt;

&lt;p&gt;All CDNs have different pros and cons and all solutions are trying to achieve the same thing: load content faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reverse proxies with caching
&lt;/h3&gt;

&lt;p&gt;Some of the CDN types you will encounter in the wild:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt; type proxies that forward all incoming traffic to your origin servers and cache as much as possible. 
Pros:

&lt;ul&gt;
&lt;li&gt;Ease of use. Your application doesn't have to be aware of the CDN itself. If you are using Cloudflare as a DNS provider you just click on the button and their servers start intercepting all traffic and caching it. On top of that, they offer a bunch of other useful services like firewalls, "page rules" that can redirect 
Cons:&lt;/li&gt;
&lt;li&gt;Can be caching too much (you don't see updates once you push because index.html is also cached).&lt;/li&gt;
&lt;li&gt;Since they are terminating connections, if they go down together with your DNS control, it becomes harder to recover.&lt;/li&gt;
&lt;li&gt;Lack of control from your side and potential security implication of allowing 3rd party to terminate TLS for you.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Push CDN
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fwztebeiizm1jtfwvmogb.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%2Fi%2Fwztebeiizm1jtfwvmogb.png" alt="Push CDN"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push CDN&lt;/strong&gt; is a setup where you upload your assets to a server (or a group of servers). An example of such CDN is Google &lt;a href="https://cloud.google.com/cdn/" rel="noopener noreferrer"&gt;Cloud CDN&lt;/a&gt;. In this setup, you will have to create a load balancer and a storage bucket and upload your content assets as part of the CI/CD pipeline where you build your frontend app. In this setup, you will need to create a new domain such as &lt;code&gt;cdn.example.com&lt;/code&gt; that points to your CDN storage location.&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You remain in control of TLS termination and have a better understanding of what content is presented when. If your frontend app uses unique IDs for the static assets, for example &lt;code&gt;/js/chunk-2d22502a.0844b32d.js&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Main file &lt;strong&gt;index.html&lt;/strong&gt; is served by your server so it can always point to the most up-to-date js/css files.&lt;/li&gt;
&lt;li&gt;You can know exactly what's pushed to the CDN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You get a new step in your CI/CD pipeline that can fail. If your frontend is deployed but assets failed to sync, your users might get a lot of errors. You also need to ensure that the CDN static files are not simply overwritten (as you might overwrite them while the old frontend app is still using previous files).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  No CDN
&lt;/h3&gt;

&lt;p&gt;No CDN, just cache control headers on your web server. This option might work for many cases, however, the first load can be painful if the user is far from your server location and you have a lot of static assets. &lt;/p&gt;

&lt;p&gt;This is still a valid option when combined with optimized asset size, PWA worker and correct caching headers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pull CDN
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fuusfjm4o3v28jo6baqna.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%2Fi%2Fuusfjm4o3v28jo6baqna.png" alt="Pull CDN"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CDNs like &lt;a href="https://bunnycdn.com/?ref=tftlx880dr" rel="noopener noreferrer"&gt;BunnyCDN&lt;/a&gt; (affiliate link, great service) pull from your origin server but don't try to proxy all your traffic. In this scenario, you serve your &lt;strong&gt;index.html&lt;/strong&gt; that then loads assets through the CDN domain instead of your own. Similarly, as with the "Push CDN" type, you will have to either serve assets from &lt;code&gt;cdn.example.com&lt;/code&gt;, or if you have a fancy global load balancer, you can configure that certain paths load files directly from CDN servers.&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ease of use. It feels like Cloudflare from the "setup" perspective. You only need to provide it with the address of your web server and then optionally configure your domain. It will pull assets and show nice stats.&lt;/li&gt;
&lt;li&gt;Pricing. It seems like it's a lot cheaper than other CDNs while providing an excellent service. They have some comparison info on their pricing page: &lt;a href="https://bunnycdn.com/pricing" rel="noopener noreferrer"&gt;https://bunnycdn.com/pricing&lt;/a&gt;, however you would need to test it for yourself as it may well depend on your content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need to ensure that your assets have unique build IDs baked into the filenames so you don't serve stale content. Fortunately, most modern javascript transpilers do this by default so in my case with Vue.js I didn't have to do anything on this front.&lt;/li&gt;
&lt;li&gt;If CDN would go down, even though your index.html loads, your assets would fail anyway. However, in this case, you would still be able to quickly change the assets domain to your main web server. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up BunnyCDN (Pull CDN) in a SPA
&lt;/h2&gt;

&lt;p&gt;I couldn't immediately spot the docs but if you are doing this not for the first time, it's quite straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a "pull zone". You will get your pull zone domain which is a reverse proxy to your origin web server:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fh23m0xfgae18l4pf684j.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%2Fi%2Fh23m0xfgae18l4pf684j.png" alt="Pull Zone Config"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;(Optional but recommended) Create a CNAME from your domain to the allocated pull zone domain (in our case it's &lt;strong&gt;cdn.webhookrelay.com&lt;/strong&gt; -&amp;gt; &lt;strong&gt;webhookrelay.b-cdn.net&lt;/strong&gt;). This enables you to load assets from your domain name. &lt;/li&gt;
&lt;li&gt;Update your webpack config to add asset file prefix. Example for vue.config.js would be:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.your-domain-here.com/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If you are using React, check out "Public Folder" docs here: &lt;a href="https://create-react-app.dev/docs/using-the-public-folder/" rel="noopener noreferrer"&gt;https://create-react-app.dev/docs/using-the-public-folder/&lt;/a&gt;. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it, generated assets will all have the prefix to load through the CDN. If you are using Nginx to serve your app, ensure that you are providing correct headers for js and css files. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;location ~* \.(?:css|js)$ {
          expires 1y;
          add_header Cache-Control "public";
          access_log off;
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope you will find this useful whenever you decide to add CDN for your website!  &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally posted on Webhook Relay blog: &lt;a href="https://webhookrelay.com/blog/2020/08/27/cdn-types-and-setup/" rel="noopener noreferrer"&gt;https://webhookrelay.com/blog/2020/08/27/cdn-types-and-setup/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>beginners</category>
      <category>vue</category>
    </item>
    <item>
      <title>Easy and secure Jenkins Operator deployment on Kubernetes</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Mon, 06 Jul 2020 10:23:45 +0000</pubDate>
      <link>https://forem.com/webhookrelay/easy-and-secure-jenkins-operator-deployment-on-kubernetes-1jn8</link>
      <guid>https://forem.com/webhookrelay/easy-and-secure-jenkins-operator-deployment-on-kubernetes-1jn8</guid>
      <description>&lt;p&gt;In this tutorial, we will configure a Jenkins pipeline on Kubernetes that leverages Jenkins and Webhook Relay operators. Jenkins Kubernetes operator will be creating Jenkins instances with a predefined seed job. Webhook Relay operator will ensure that GitHub webhooks on push events trigger new Jenkins builds for a fast and efficient CI/CD experience.&lt;/p&gt;

&lt;p&gt;Advantages of this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your Jenkins instance is only accessible through kubectl port-forward while maintaining the ability to receive webhooks from public destinations.&lt;/li&gt;
&lt;li&gt;Jenkins pipeline configuration is stored in Git.&lt;/li&gt;
&lt;li&gt;Webhook Relay routing configuration is stored in Git, the same as the Jenkins itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can read about operator pattern in &lt;a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/"&gt;Kubernetes docs&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Prerequisites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.helm.sh/using_helm/#installing-helm"&gt;Helm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://my.webhookrelay.com"&gt;Webhook Relay account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/"&gt;Kubernetes&lt;/a&gt; environment, Minikube, k3s, GKE, AKS, etc. are fine.&lt;/li&gt;
&lt;li&gt;Configured kubectl&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;The installation will consist of several steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installing Jenkins operator&lt;/li&gt;
&lt;li&gt;Installing Webhook Relay operator&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Create a fresh namespace
&lt;/h2&gt;

&lt;p&gt;Let's start by creating a new namespace where we will put our Jenkins instance and run builds. I will call it 'jenkins' but you can choose any other name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace jenkins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then switch to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config set-context &lt;span class="si"&gt;$(&lt;/span&gt;kubectl config current-context&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;jenkins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Jenkins Operator
&lt;/h2&gt;

&lt;p&gt;We will install Jenkins operator using Helm. First, add the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add jenkins https://raw.githubusercontent.com/jenkinsci/kubernetes-operator/master/chart
helm repo update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the repository has been added, install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;jenkins-operator jenkins/jenkins-operator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Official docs can be found here: &lt;a href="https://jenkinsci.github.io/kubernetes-operator/docs/installation/"&gt;https://jenkinsci.github.io/kubernetes-operator/docs/installation/&lt;/a&gt;. The operator is not the Jenkins itself so to get our Jenkins instance, we will have to create a &lt;a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/"&gt;Custom Resource&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Custom resources are extensions of the Kubernetes API. This page discusses when to add a custom resource to your Kubernetes cluster and when to use a standalone service. It describes the two methods for adding custom resources and how to choose between them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Start Jenkins (using Custom Resource)
&lt;/h2&gt;

&lt;p&gt;We will need to create a CR. You can either use Jenkins Operator docs to create one or you can fork this &lt;a href="https://github.com/webhookrelay/jenkins-operator-example.git"&gt;https://github.com/webhookrelay/jenkins-operator-example.git&lt;/a&gt; repository and clone it. Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update &lt;strong&gt;jenkins_cr.yaml&lt;/strong&gt; &lt;a href="https://github.com/webhookrelay/jenkins-operator-example/blob/master/jenkins_cr.yaml"&gt;file&lt;/a&gt; &lt;code&gt;https://github.com/webhookrelay/jenkins-operator-example.git&lt;/code&gt; to your own repository fork (it will usually be &lt;code&gt;https://github.com/&amp;lt;your username or organization&amp;gt;/jenkins-operator-example.git&lt;/code&gt;). &lt;/li&gt;
&lt;li&gt;Create it with kubectl:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; jenkins_cr.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Main differences in this file from the stock example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added &lt;code&gt;github&lt;/code&gt; plugin as we will need it to trigger jobs&lt;/li&gt;
&lt;li&gt;Seed job got &lt;code&gt;githubPushTrigger: true&lt;/code&gt; set as well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Creating this PR should result in two additional containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
NAME                                      READY   STATUS    RESTARTS   AGE
jenkins-jenkins                           1/1     Running   0          7m11s
jenkins-operator-6dbbc458c9-gmx6p         1/1     Running   0          18m
seed-job-agent-jenkins-65cc4bc684-9ztr5   1/1     Running   0          6m21s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's connect to Jenkins. First, get username and password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;--namespace&lt;/span&gt; jenkins get secret jenkins-operator-credentials-jenkins &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'jsonpath={.data.user}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
kubectl &lt;span class="nt"&gt;--namespace&lt;/span&gt; jenkins get secret jenkins-operator-credentials-jenkins &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'jsonpath={.data.password}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in one terminal start port forwarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward jenkins-jenkins 8080:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then just open &lt;a href="http://localhost:8080"&gt;http://localhost:8080&lt;/a&gt; in your browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eD9s8cT2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/qonhowe4fy0r5ndml1ee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eD9s8cT2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/qonhowe4fy0r5ndml1ee.png" alt="Jenkins dashboard" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhook Relay
&lt;/h2&gt;

&lt;p&gt;Retrieve your access token key &amp;amp; secret pair from &lt;a href="https://my.webhookrelay.com/tokens"&gt;https://my.webhookrelay.com/tokens&lt;/a&gt; and set them as an environment variables:&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;export &lt;/span&gt;&lt;span class="nv"&gt;RELAY_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxxxxxxxxxxx
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RELAY_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add Webhook Relay Operator Helm repository and install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add webhookrelay https://charts.webhookrelay.com
helm repo update
helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; webhookrelay-operator &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;jenkins webhookrelay/webhookrelay-operator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; credentials.key&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$RELAY_KEY&lt;/span&gt; &lt;span class="nt"&gt;--set&lt;/span&gt; credentials.secret&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$RELAY_SECRET&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Operator doesn't forward webhooks on its own. Each created CR will ensure an agent deployment that is configured to route specific buckets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From the &lt;a href="https://github.com/webhookrelay/jenkins-operator-example/blob/master/webhookrelay_cr.yaml"&gt;operator example repository&lt;/a&gt; we will need to create Webhook Relay Custom Resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; webhookrelay_cr.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note that if you have modified Jenkins CR name you will need to update webhookrelay_cr.yaml "destination" field from &lt;code&gt;destination: http://jenkins-operator-http-jenkins:8080/github-webhook/&lt;/code&gt; to whatever your current Jenkins service is. Typically it will be in a format &lt;code&gt;jenkins-operator-http-&amp;lt;CR name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  GitHub Configuration
&lt;/h2&gt;

&lt;p&gt;This step could be automated by making Jenkins automatically configure Github repositories to forwarding to this endpoint, however for simplicity and so that it's more clear how it works, we will add this URL manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get your Webhook Relay public URL
&lt;/h3&gt;

&lt;p&gt;To get your public endpoint you can either visit &lt;a href="https://my.webhookrelay.com/buckets"&gt;https://my.webhookrelay.com/buckets&lt;/a&gt; page or get it via CR status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get webhookrelayforwards.forward.webhookrelay.com forward-to-jenkins &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'jsonpath={.status.publicEndpoints[0]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result should look something like:&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="nv"&gt;$ &lt;/span&gt;kubectl get webhookrelayforwards.forward.webhookrelay.com forward-to-jenkins &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'jsonpath={.status.publicEndpoints[0]}'&lt;/span&gt;
https://k0yv9ip5sxxp55ncsu936k.hooks.webhookrelay.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add public URL to GitHub repository settings
&lt;/h3&gt;

&lt;p&gt;Take the public endpoint URL and add it to your GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ax9ikjbT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/g16gtp9hjdjk6k6ouir0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ax9ikjbT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/g16gtp9hjdjk6k6ouir0.png" alt="GitHub configuration" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the pipeline
&lt;/h2&gt;

&lt;p&gt;First, when the pipeline is created, trigger the build manually. After that, any push to your GitHub repository will send a webhook through Webhook Relay to your Jenkins instance that's running inside a Kubernetes cluster:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--O8Fz5ZZM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/x1fw832ss58naya1nc1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--O8Fz5ZZM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/x1fw832ss58naya1nc1j.png" alt="Jenkins pipeline" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This tutorial was initially published on Webhook Relay website: &lt;a href="https://webhookrelay.com/v1/tutorials/webhooks-jenkins-operator-kubernetes.html"&gt;https://webhookrelay.com/v1/tutorials/webhooks-jenkins-operator-kubernetes.html&lt;/a&gt;&lt;/strong&gt; &lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>github</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Simple trick to get specific elements from Kubernetes kubectl output</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Sun, 05 Jul 2020 18:09:10 +0000</pubDate>
      <link>https://forem.com/krusenas/simple-trick-to-get-specific-elements-from-kubernetes-kubectl-output-3502</link>
      <guid>https://forem.com/krusenas/simple-trick-to-get-specific-elements-from-kubernetes-kubectl-output-3502</guid>
      <description>&lt;p&gt;Usually, most Kubernetes objects are quite big as they have namespace, name, labels, template, spec and a bunch of other fields. When you need to get only one or several fields, it can get tedious scrolling up and down in your terminal looking for it :) Luckily, &lt;code&gt;kubectl&lt;/code&gt; supports formatting your output using JSON path that allows you to specify the format and values yourself.&lt;/p&gt;

&lt;p&gt;In the following example I will show how to format a Custom Resource output from the &lt;a href="https://github.com/webhookrelay/webhookrelay-operator/"&gt;operator that I have recently wrote&lt;/a&gt; so it's easy to retrieve data from the status field.&lt;/p&gt;

&lt;p&gt;First, let's see what data is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get  webhookrelayforwards.forward.webhookrelay.com -o json
{
    "apiVersion": "v1",
    "items": [
        {
            "apiVersion": "forward.webhookrelay.com/v1",
            "kind": "WebhookRelayForward",
            "metadata": {
                "annotations": {
                    "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"forward.webhookrelay.com/v1\",\"kind\":\"WebhookRelayForward\",\"metadata\":{\"annotations\":{},\"name\":\"forward-to-jenkins\",\"namespace\":\"jenkins\"},\"spec\":{\"buckets\":[{\"inputs\":[{\"description\":\"Endpoint for GitHub\",\"name\":\"public-endpoint\",\"responseBody\":\"OK\",\"responseStatusCode\":200}],\"name\":\"jenkins-whr-operator\",\"outputs\":[{\"destination\":\"http://jenkins-operator-http-jenkins:8080/github-webhook/\",\"name\":\"jenkins\"}]}],\"image\":\"webhookrelay/webhookrelayd-ubi8:latest\"}}\n"
                },
                "creationTimestamp": "2020-07-05T12:52:11Z",
                "generation": 2,
                "name": "forward-to-jenkins",
                "namespace": "jenkins",
                "resourceVersion": "270615",
                "selfLink": "/apis/forward.webhookrelay.com/v1/namespaces/jenkins/webhookrelayforwards/forward-to-jenkins",
                "uid": "9d75809c-1795-45e2-a437-bb89b22bdac4"
            },
            "spec": {
                "buckets": [
                    {
                        "inputs": [
                            {
                                "description": "Endpoint for GitHub",
                                "name": "public-endpoint",
                                "responseBody": "OK",
                                "responseStatusCode": 200
                            }
                        ],
                        "name": "jenkins-whr-operator",
                        "outputs": [
                            {
                                "destination": "http://jenkins-operator-http-jenkins:8080/github-webhook/",
                                "name": "jenkins"
                            }
                        ]
                    }
                ],
                "image": "webhookrelay/webhookrelayd-ubi8:latest"
            },
            "status": {
                "agentStatus": "Running",
                "publicEndpoints": [
                    "https://k0yv9ip5sxxp55ncsu936k.hooks.webhookrelay.com"
                ],
                "ready": true,
                "routingStatus": "Configured"
            }
        }
    ],
    "kind": "List",
    "metadata": {
        "resourceVersion": "",
        "selfLink": ""
    }
}

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

&lt;/div&gt;



&lt;p&gt;Here, if we want to grab just the value from &lt;code&gt;publicEndpoints&lt;/code&gt;, we specify again the resource and additional &lt;code&gt;jsonpath&lt;/code&gt; query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get webhookrelayforwards.forward.webhookrelay.com forward-to-jenkins -o 'jsonpath={.status.publicEndpoints[0]}'
https://k0yv9ip5sxxp55ncsu936k.hooks.webhookrelay.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go ahead and try it on your own Kubernetes cluster! :) &lt;/p&gt;

&lt;p&gt;You can find more examples in the official docs here: &lt;a href="https://kubernetes.io/docs/reference/kubectl/jsonpath/"&gt;https://kubernetes.io/docs/reference/kubectl/jsonpath/&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>productivity</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>Serverless webhook transformation (DockerHub to Slack)</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Fri, 28 Feb 2020 08:49:56 +0000</pubDate>
      <link>https://forem.com/webhookrelay/serverless-webhook-transformation-dockerhub-to-slack-4j9d</link>
      <guid>https://forem.com/webhookrelay/serverless-webhook-transformation-dockerhub-to-slack-4j9d</guid>
      <description>&lt;p&gt;Many Docker registries provide a way to notify team in chat channels when new images are pushed (if you are waiting for a build complete). Let's add this capability to the official DockerHub registry! :) &lt;/p&gt;

&lt;p&gt;We will use newly released &lt;a href="https://webhookrelay.com/v1/guide/functions.html"&gt;Webhook Relay Functions&lt;/a&gt; to transform webhooks while they are passing through the service.&lt;/p&gt;

&lt;p&gt;Prerequisites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://my.webhookrelay.com/"&gt;Webhook Relay account&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hub.docker.com/"&gt;DockerHub account&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create a bucket and configure DockerHub notification
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a bucket here &lt;a href="https://my.webhookrelay.com/buckets"&gt;https://my.webhookrelay.com/buckets&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once you have it, in the inputs section you will find your public input endpoint, copy it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Bek8hc5u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/hb0nbhnm0foyazklue94.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Bek8hc5u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/hb0nbhnm0foyazklue94.png" alt="Input endpoint URL" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add a new DockerHub webhook setting pointing at our public input endpoint (&lt;a href="https://docs.docker.com/docker-hub/webhooks/"&gt;DockerHub docs&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7USp9W5X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4v3w4vhr6tt757a8gppd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7USp9W5X--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4v3w4vhr6tt757a8gppd.png" alt="DockerHub config" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Get a sample of DockerHub webhook
&lt;/h3&gt;

&lt;p&gt;Push a new Docker image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker push karolisr/demo-webhook:latest
The push refers to repository [docker.io/karolisr/demo-webhook]
48bd38e03c42: Mounted from karolisr/webhook-demo
fd9f9fbd5947: Mounted from karolisr/webhook-demo
5216338b40a7: Mounted from karolisr/webhook-demo
latest: digest: sha256:703f2bab2ce8df0c5ec4e45e26718954b09bf4a625ab831c6556fd27d60f1325 size: 949
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should be able to see a new incoming webhook. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"push_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pushed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1582839308&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"images"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pusher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karolisr"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"callback_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://registry.hub.docker.com/u/karolisr/demo-webhook/hook/242ii4ddc2jji4a0cc44fbcdbcdecj1ab/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"is_trusted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"full_description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"repo_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://hub.docker.com/r/karolisr/demo-webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karolisr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"is_official"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"is_private"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo-webhook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karolisr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"star_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"comment_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"date_created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1524557040&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"repo_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karolisr/demo-webhook"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a Function to transform the webhook
&lt;/h3&gt;

&lt;p&gt;Go to the &lt;a href="https://my.webhookrelay.com/functions"&gt;Functions page&lt;/a&gt; and click on a "Create Function" button. Enter a name, for example "dockerhub-to-slack" and click "Submit".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For this example we are using &lt;a href="https://www.lua.org/start.html"&gt;Lua language&lt;/a&gt; which provides an easy way to parse, transform payloads. Webhook Relay Functions also support WebAssembly modules, however they are currently under development and can only be added/updated using relay CLI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can now copy/paste webhook payload into the "request body" area for later tests. In the code editor let's add a Lua function to get repository name and prepare a Slack webhook payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="nb"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"New image pushed at: "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;"repo_name"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;":"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"push_data"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;-- Preparing Slack payload&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"in_channel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- Set request header to application/json&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Set request method to PUT&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Set modified request body&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;SetRequestBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click "Save" and then try testing it with the "Send" button:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PJjqKf0Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/icxxw26h9fhifeqnqfxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PJjqKf0Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/icxxw26h9fhifeqnqfxy.png" alt="Function invoke example" width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect everything together
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Navigate to &lt;a href="https://api.slack.com/messaging/webhooks"&gt;https://api.slack.com/messaging/webhooks&lt;/a&gt; and click "Create your Slack app". Select your workspace, enter a name that you will remember.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a new incoming webhook configuration, copy "Webhook URL" (it starts with &lt;code&gt;https://hooks.slack.com/services/T3...&lt;/code&gt;), we will need to supply it to Webhook Relay.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open your bucket details (via &lt;a href="https://my.webhookrelay.com/buckets"&gt;https://my.webhookrelay.com/buckets&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open "OUTPUT DESTINATIONS" tab and create a new output called "Slack" with the Slack URL from step 2:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LtkSkR_8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/byd869wvndrrj1po5ioo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LtkSkR_8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/byd869wvndrrj1po5ioo.png" alt="Create destination" width="800" height="303"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Once created, click on the "code" symbol and from the dropdown select &lt;code&gt;dockerhub_to_slack&lt;/code&gt; function:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g6-kMyCo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4zl0bug70v022dv01xjd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g6-kMyCo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4zl0bug70v022dv01xjd.png" alt="Select function" width="800" height="199"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Push new image to DockerHub, you should see a new notification in your Slack channel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NADNc-tN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/prkk6jdldn4srmsmanbl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NADNc-tN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/prkk6jdldn4srmsmanbl.png" alt="Slack notification" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it, feel free to continue modifying Lua function to include pusher's name and message format. Following this process you can transform any webhook into any other webhook.&lt;/p&gt;

&lt;p&gt;Have fun!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally released on &lt;a href="https://webhookrelay.com/v1/examples/convert-dockerhub-webhook-to-slack.html"&gt;https://webhookrelay.com/v1/examples/convert-dockerhub-webhook-to-slack.html&lt;/a&gt;&lt;/em&gt; &lt;/p&gt;

</description>
      <category>serverless</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>docker</category>
    </item>
    <item>
      <title>Sunstone - simple templates for Kubernetes and beyond</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Tue, 19 Nov 2019 16:33:36 +0000</pubDate>
      <link>https://forem.com/krusenas/sunstone-simple-templates-for-kubernetes-and-beyond-1pe1</link>
      <guid>https://forem.com/krusenas/sunstone-simple-templates-for-kubernetes-and-beyond-1pe1</guid>
      <description>&lt;p&gt;Sunstone is an easy to use, no-CLI solution to create templates that work with plain &lt;code&gt;curl&lt;/code&gt;. It's targeted at users that need some templating functionality but don't want to use tools like Helm that need their own registries, directory structures and update mechanisms.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Currently &lt;a href="https://about.sunstone.dev"&gt;Sunstone&lt;/a&gt; is targeted at Kubernetes users but there's actually no restriction or issues when using it with any other tools (it doesn't really care whether templates are Kubernetes manifests or Docker Compose files or anything else).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Project goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users host templates on their own infrastucture that is reachable from Sunstone (GitHub, GitLab, S3, NGINX static file server).&lt;/li&gt;
&lt;li&gt;Easy to use and &lt;strong&gt;open&lt;/strong&gt; alias system where users can share their own templates. We could potentially allow hub yaml manifest to point to other hubs and discover aliases from them.
&lt;/li&gt;
&lt;li&gt;When fetch is not possible, user can create and update private repo in the Sunstone via &lt;code&gt;curl&lt;/code&gt; or a specialized Docker container (currently works as a GitHub action but a standalone instructions will be available) that can be included as a CI step.&lt;/li&gt;
&lt;li&gt;Dead simple installation instructions for your docs. For example, to install a &lt;a href="https://dotscience.com/"&gt;Dotscience&lt;/a&gt; ML model deployment operator for your user account, it's as simple as:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   kubectl apply -f https://sunstone.dev/dotscience?token=my-super-secret-token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes deployment page in &lt;a href="https://cloud.dotscience.com/deployers"&gt;dotscience.com deployers page&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NAVfVt1T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/ydp85bocyutqafmjityh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NAVfVt1T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/ydp85bocyutqafmjityh.png" alt="Deployer install via Sunstone" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: Remote template
&lt;/h2&gt;

&lt;p&gt;Here we have a template hosted on GitHub at: &lt;a href="https://github.com/sunstone-dev/example/blob/master/deployment.yaml"&gt;https://github.com/sunstone-dev/example/blob/master/deployment.yaml&lt;/a&gt;. Contents are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;pushwf&lt;/span&gt;  
  &lt;span class="na"&gt;labels&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pushwf"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;revisionHistoryLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pushwf&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&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;pushwf&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pushwf&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                    
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keelhq/push-workflow-example:{{ .version | latestRegistrySemver "keelhq/push-workflow-example" }}&lt;/span&gt;
          &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Always&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;pushwf&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.port | default 8500&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
          &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
              &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.port | default 8500&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
            &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
            &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;    

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

&lt;/div&gt;



&lt;p&gt;(raw link is: &lt;a href="https://raw.githubusercontent.com/sunstone-dev/example/master/deployment.yaml"&gt;https://raw.githubusercontent.com/sunstone-dev/example/master/deployment.yaml&lt;/a&gt;)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Have a look at &lt;code&gt;{{ .version | latestRegistrySemver "keelhq/push-workflow-example" }}&lt;/code&gt;, this &lt;code&gt;latestRegistrySemver&lt;/code&gt; template tag will actually go to that registry and retrieve the latest semver tag from the registry :) No need to update your docs when a new semver image tag is released!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, to render a template we need to know that Sunstone template generator API works like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://sunstone.dev/&amp;lt;URL to link but without https&amp;gt;?&amp;lt;first arg&amp;gt;=&amp;lt;value&amp;gt;&amp;amp;&amp;lt;second arg&amp;gt;=&amp;lt;value&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, to render our example template &lt;strong&gt;with default values&lt;/strong&gt;, use:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sunstone.dev/raw.githubusercontent.com/sunstone-dev/example/master/deployment.yaml"&gt;https://sunstone.dev/raw.githubusercontent.com/sunstone-dev/example/master/deployment.yaml&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Result should look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;pushwf&lt;/span&gt;  
  &lt;span class="na"&gt;labels&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pushwf"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;revisionHistoryLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pushwf&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&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;pushwf&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pushwf&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                    
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keelhq/push-workflow-example:0.11.0-alpha&lt;/span&gt;
          &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Always&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;pushwf&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8500&lt;/span&gt;
          &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
              &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8500&lt;/span&gt;
            &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
            &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you add &lt;code&gt;?port=999&lt;/code&gt; to the URL:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sunstone.dev/raw.githubusercontent.com/sunstone-dev/example/master/deployment.yaml?port=9999"&gt;https://sunstone.dev/raw.githubusercontent.com/sunstone-dev/example/master/deployment.yaml?port=9999&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Endpoint will show you different container port:&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="nn"&gt;...&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8500&lt;/span&gt;
    &lt;span class="err"&gt;      &lt;/span&gt;&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kubectl&lt;/code&gt; allows to install directly from this URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://sunstone.dev/raw.githubusercontent.com/sunstone-dev/example/master/deployment.yaml?port=9999
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 2: Using aliases
&lt;/h2&gt;

&lt;p&gt;Constructing whole URL is usually quite difficult to remember, that's why we have a public aliases "hub" that allows everyone to map their own shorter aliases to remote repositories. Hub repository can be found here: &lt;a href="https://github.com/sunstone-dev/hub"&gt;https://github.com/sunstone-dev/hub&lt;/a&gt;. If you would like to contribute your own template, just fork it and submit a pull request. &lt;/p&gt;

&lt;p&gt;To install from alias, it becomes as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://sunstone.dev/keel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you can also view all public aliases here: &lt;a href="https://apps.sunstone.dev/dashboard"&gt;https://apps.sunstone.dev/dashboard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking "Install" on any:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cF829c12--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/bnr6lv0xhhykgkrnq1yw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cF829c12--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/bnr6lv0xhhykgkrnq1yw.png" alt="Install button on Sunstone generates linsk" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Entering into variable fields will automatically generate your template render URL:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--F1Akrt7H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/aec0tw523sl76mrhx5og.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--F1Akrt7H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/aec0tw523sl76mrhx5og.gif" alt="Entering variables updates the render link" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Next steps
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Check out Sunstone template docs: &lt;a href="https://about.sunstone.dev/examples/#latest-docker-semver-tag"&gt;https://about.sunstone.dev/examples/#latest-docker-semver-tag&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Not yet documented, but you can check out private repositories (requires login with GitHub just for authentication, no access to repos): &lt;a href="https://apps.sunstone.dev/private-repositories"&gt;https://apps.sunstone.dev/private-repositories&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Accompanying GitHub action to automatically update your private alias on Sunstone when you push to that repo: &lt;a href="https://github.com/marketplace/actions/sunstone-template-update"&gt;https://github.com/marketplace/actions/sunstone-template-update&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Tech stack
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Golang for the backend (API, templating)&lt;/li&gt;
&lt;li&gt;Vue.js&lt;/li&gt;
&lt;li&gt;Database - Firestore on GCP&lt;/li&gt;
&lt;li&gt;Running on GKE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;P.S. &lt;/p&gt;

&lt;p&gt;Sunstone templating API will be Open Source (still need to do some work to separate it from the current multi-tenant service) so you will be able to just host it anywhere. It might be possible to also run it via Cloud Run or similar services. &lt;/p&gt;

</description>
      <category>showdev</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A simple way to keep your Vue page title in sync with the router</title>
      <dc:creator>Karolis</dc:creator>
      <pubDate>Sun, 08 Sep 2019 22:11:10 +0000</pubDate>
      <link>https://forem.com/webhookrelay/a-simple-way-to-keep-your-vue-page-title-in-sync-with-the-router-ec0</link>
      <guid>https://forem.com/webhookrelay/a-simple-way-to-keep-your-vue-page-title-in-sync-with-the-router-ec0</guid>
      <description>&lt;p&gt;I have noticed in quite a few projects that developers forget to keep page title updated with the router or maybe think that they will do it tomorrow as it is such a small feature :). It always makes sense to keep the title synchronized with the contents for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;helps users with more than one tab &lt;/li&gt;
&lt;li&gt;important for website analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will show you how to do it with the standard &lt;a href="https://github.com/vuejs/vue-router"&gt;vue-router&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Declare route meta in your router config
&lt;/h2&gt;

&lt;p&gt;First things first, let's add some additional metadata to our standard routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoginPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/buckets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;buckets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BucketsPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Buckets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More docs on the router package can be found &lt;a href="https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes"&gt;in the official vue router website&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add $route watcher in your App.vue
&lt;/h2&gt;

&lt;p&gt;Go to your Vue main .vue file (it should have a &lt;code&gt;&amp;lt;router-view&amp;gt;&amp;lt;/router-view&amp;gt;&lt;/code&gt; component) and add a watcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$route&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Website&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This will set a title as your page title (if you specified a meta field for that route). I have tried to achieve the same with router navigation guards but &lt;code&gt;$route&lt;/code&gt; watcher seemed like the simplest solution. &lt;/p&gt;

&lt;p&gt;Looks easy, right? :) Feel free to experiment and define more fields in the router meta like page description and anything else you want to be set for that page. Then modify the watcher to also use them. &lt;/p&gt;

&lt;p&gt;Hope this helps!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>vue</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
