<?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: Chris Rowley</title>
    <description>The latest articles on Forem by Chris Rowley (@clubside).</description>
    <link>https://forem.com/clubside</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%2F427503%2F25bc02b8-7c2f-4972-8269-740a841bb23a.jpeg</url>
      <title>Forem: Chris Rowley</title>
      <link>https://forem.com/clubside</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/clubside"/>
    <language>en</language>
    <item>
      <title>Caddy, Go, Docker and a Single Page App</title>
      <dc:creator>Chris Rowley</dc:creator>
      <pubDate>Mon, 03 Apr 2023 16:11:39 +0000</pubDate>
      <link>https://forem.com/clubside/caddy-go-docker-and-a-single-page-app-5g40</link>
      <guid>https://forem.com/clubside/caddy-go-docker-and-a-single-page-app-5g40</guid>
      <description>&lt;p&gt;On a recent project I was tasked to create a Golang-based web service and a Single Page App to go with it. The company wasn't set on deployment so I decided to package things in a way that would best simulate a production environment while retaining the ability to launch and test the SPA from any machine. The big sticking point to this simulation was &lt;code&gt;https&lt;/code&gt; connectivity. While the &lt;a href="https://caddyserver.com" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt; server was something I'm familiar with, using it along with the &lt;a href="https://go.dev/" rel="noopener noreferrer"&gt;Go&lt;/a&gt; API server would require two shells and a platform-specific version of Caddy. With more than a few questions lingering I decided to try out Docker as a single deployment point for the project.&lt;/p&gt;

&lt;p&gt;This article isn't about any of the technologies involved. I'm trying to revisit my roots and write a comprehensive tutorial on the project. There is a &lt;a href="https://clubside.github.io/docker-caddy-go-api/" rel="noopener noreferrer"&gt;work-in-progress version&lt;/a&gt; as well as the repository at &lt;a href="https://github.com/clubside/docker-caddy-go-api" rel="noopener noreferrer"&gt;Github&lt;/a&gt;. No, here I'm going to assume the reader is familiar with Go, SPAs, Caddy and Docker, and is looking for a method to tie them all together in a &lt;code&gt;localhost&lt;/code&gt; environment. Non-standard ports are used to avoid competing with other web services. These steps have been tested on Windows but should be adaptable to other operating systems.&lt;/p&gt;

&lt;p&gt;We will be serving our SPA from a folder &lt;code&gt;public&lt;/code&gt; in the root of the project. The root also contains our Go-based API. We also need to configure Caddy to generate TLS certificates, reverse proxy our API and serve our SPA static files. This &lt;code&gt;Caddyfile&lt;/code&gt; handles our needs:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&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="err"&gt;http_port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2010&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;servers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2015&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="err"&gt;listener_wrappers&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="err"&gt;http_redirect&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="err"&gt;tls&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;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;https://localhost:&lt;/span&gt;&lt;span class="mi"&gt;2015&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="err"&gt;encode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;zstd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;gzip&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="err"&gt;handle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/*&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="err"&gt;reverse_proxy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;localhost:&lt;/span&gt;&lt;span class="mi"&gt;3000&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="err"&gt;handle&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="err"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;public&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;try_files&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;index.html&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;file_server&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;p&gt;Caddy needs an &lt;code&gt;http&lt;/code&gt; port so it doesn't try to bind with &lt;code&gt;:80&lt;/code&gt; but the &lt;code&gt;http_redirect&lt;/code&gt; listener will ensure only &lt;code&gt;https&lt;/code&gt; will be served off of port &lt;code&gt;:2015&lt;/code&gt;. Caddy will redirect calls to the &lt;code&gt;/api&lt;/code&gt; path to our Go API, will use the &lt;code&gt;public&lt;/code&gt; folder as the root of the site, and deliver &lt;code&gt;index.html&lt;/code&gt; as the SPA when a named resource isn't available.&lt;/p&gt;

&lt;p&gt;After installing &lt;a href="https://www.docker.com/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt; and having it running we can develop a &lt;code&gt;Dockerfile&lt;/code&gt; to execute commands needed to construct our environment as well as a &lt;code&gt;docker-compose.yml&lt;/code&gt; file to define how the two containers, Caddy and our API, are accessible from the package.&lt;/p&gt;

&lt;p&gt;The final addition to our formula is the introduction of &lt;a href="https://github.com/cosmtrek/air" rel="noopener noreferrer"&gt;☁️ Air - Live reload for Go apps&lt;/a&gt;. With this or a similar tool we can keep Docker running after making changes to our API server. To configure Air we need a &lt;code&gt;.air.toml&lt;/code&gt; file:&lt;/p&gt;

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

&lt;span class="nn"&gt;[build]&lt;/span&gt;
&lt;span class="py"&gt;cmd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"go build -o ./tmp/spa ."&lt;/span&gt;
&lt;span class="py"&gt;bin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tmp/spa"&lt;/span&gt;
&lt;span class="py"&gt;exclude_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"docs"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This is fairly basic, issuing &lt;code&gt;go build&lt;/code&gt; command and directing the output to a &lt;code&gt;tmp&lt;/code&gt; folder; pointing to that folder as the location of the executable; and excluding the folders that are not part of the Go-based API server.&lt;/p&gt;

&lt;p&gt;While Air provides its own image we're going to use the basic Go version instead so we need to establish the environment using a &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;/p&gt;

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

FROM golang:1.19

WORKDIR /app

RUN go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/cosmtrek/air@latest

COPY go.mod ./
RUN go mod download

CMD &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"air"&lt;/span&gt;, &lt;span class="s2"&gt;"-c"&lt;/span&gt;, &lt;span class="s2"&gt;".air.toml"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Here we're requesting an image of 1.19 version of Go and setting a working folder for the container. Into this folder we're installing Air, copying &lt;code&gt;go.mod&lt;/code&gt; and downloading relevant modules. Finally we're starting up Air and loading our configuration file. These actions will serve as the basis for our Go container. For better explanations of each of these directives please check out the official documentation &lt;a href="https://docs.docker.com/engine/reference/builder/" rel="noopener noreferrer"&gt;Dockerfile reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We're almost there, just the Docker Compose configuration &lt;code&gt;docker-compose.yml&lt;/code&gt; file remains:&lt;/p&gt;

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

&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;go&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000"&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;./:/app&lt;/span&gt;
  &lt;span class="na"&gt;caddy&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;caddy:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;on-failure&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2010:2010"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2015:2015"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2019:2019"&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;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./public:/srv&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_config:/config&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;caddy_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Our two containers, described as services here, take advantage of both a &lt;code&gt;Dockerfile&lt;/code&gt; build and a Docker image. Our &lt;code&gt;go&lt;/code&gt; service uses the &lt;code&gt;build&lt;/code&gt; directive set to the root of our project from which it uses the &lt;code&gt;Dockerfile&lt;/code&gt; setup and exposes our API server on port 3000. Our &lt;code&gt;caddy&lt;/code&gt; service uses official image, requesting the latest version. As this image is defined by another party it has specific parameters necessary for its container to operate. The &lt;code&gt;ports&lt;/code&gt; correspond to our definitions in &lt;code&gt;Caddyfile&lt;/code&gt; with one addition, &lt;code&gt;2019:2019&lt;/code&gt; which maps to Caddy's admin API. This is necessary to store the SSL certificate after Docker is up and running.&lt;/p&gt;

&lt;p&gt;Caddy uses a number of &lt;code&gt;volumes&lt;/code&gt;. Two point directly at files within our project, first our &lt;code&gt;Caddyfile&lt;/code&gt;, then our &lt;code&gt;public&lt;/code&gt; folder which Caddy will serve live files. The other two are virtual filesystems Docker will create as defined by the master &lt;code&gt;volumes&lt;/code&gt; parameters. We can assume the &lt;code&gt;caddy_config&lt;/code&gt; volume is where active configuration is stored as it is not discussed on the &lt;a href="https://hub.docker.com/_/caddy" rel="noopener noreferrer"&gt;Caddy Docker Official Image&lt;/a&gt; page, so we're copying their parameter exactly, but the &lt;code&gt;caddy_data&lt;/code&gt; volume needs some extra discussion. It is used to store a number of things including SSL certificates. By default Docker creates and destroys volumes upon startup and exit. As we want to persist our certificate across sessions we can take advantage of an &lt;code&gt;external&lt;/code&gt; Docker volume. These virtual filesystems are created before starting the Docker session for the first time. This can be done from the command line or more easily from within the Docker Desktop app. Simply choose "Volumes", click the "Create" button and specify &lt;code&gt;caddy_data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We're now ready to start up our new environment by entering this in a terminal:&lt;/p&gt;

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

docker compose up


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

&lt;/div&gt;

&lt;p&gt;Here's where the &lt;code&gt;external&lt;/code&gt; volume and Caddy's admin API come into play. Make sure you have a local copy of Caddy and navigate to its folder and execute:&lt;/p&gt;

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

caddy trust &lt;span class="nt"&gt;--address&lt;/span&gt; localhost:2019


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

&lt;/div&gt;

&lt;p&gt;This should present a certificate confirmation dialog such as 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%2Fm9n2yyprk2owczz20u75.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%2Fm9n2yyprk2owczz20u75.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The SSL information gets stored in the Docker &lt;code&gt;caddy_data&lt;/code&gt; volume so it will be available any time we start up our package. The fruits of our labor aren't readily visible when we visit &lt;code&gt;https://localhost:2015&lt;/code&gt; again. But under the hood we can develop any part of our project and have the changes automatically updated in a local version of a production environment.&lt;/p&gt;

&lt;p&gt;Now it's time to take a break knowing that at any point you can fire up &lt;code&gt;docker compose up&lt;/code&gt; and continue to develop your API or SPA with live-reloading and in a secure server environment.&lt;/p&gt;

</description>
      <category>go</category>
      <category>docker</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
