<?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>DEV Community: James Hunt</title>
    <description>The latest articles on DEV Community by James Hunt (@jhunt).</description>
    <link>https://dev.to/jhunt</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F610840%2Fd133415c-fda4-48b8-b2f6-88b103b37415.png</url>
      <title>DEV Community: James Hunt</title>
      <link>https://dev.to/jhunt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jhunt"/>
    <language>en</language>
    <item>
      <title>Implementing Non-Trivial Containerized Systems Part 4: Adding a Web Interface</title>
      <dc:creator>James Hunt</dc:creator>
      <pubDate>Tue, 22 Jun 2021 18:07:03 +0000</pubDate>
      <link>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-part-4-adding-a-web-interface-3o6a</link>
      <guid>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-part-4-adding-a-web-interface-3o6a</guid>
      <description>&lt;p&gt;This is the fourth part &lt;a href="https://starkandwayne.com/blog/implementing-non-trivial-containerized-systems/"&gt;of a&lt;/a&gt; &lt;a href="https://starkandwayne.com/blog/implementing-non-trivial-containerized-systems-part-1-picking-components/"&gt;multi&lt;/a&gt;-&lt;a href="https://starkandwayne.com/blog/implementing-non-trivial-containerized-systems-part-2-containerizing-with-docker/"&gt;part&lt;/a&gt; &lt;a href="https://starkandwayne.com/blog/implementing-non-trivial-containerized-systems-part-3-deploying-containers-together/"&gt;series&lt;/a&gt; on designing and building non-trivial containerized solutions.  We're making a radio station using off-the-shelf components and some home-spun software, all on top of Docker, Docker Compose, and eventually, Kubernetes.&lt;/p&gt;

&lt;p&gt;In this part, we've got a working system, our own rebuildable images, and a portable Docker Compose deployment recipe.  What more could we want?  A web interface for managing the tracks that we're streaming would be nice...&lt;br&gt;
The Icecast web interface is, well, &lt;em&gt;usable&lt;/em&gt;, but it does leave a fair amount to be desired.  This is the part of systems design where I usually take a step back, see what's missing, and then focus my software development energies on solving &lt;em&gt;those&lt;/em&gt; deficiencies.&lt;/p&gt;

&lt;p&gt;As a radio station operator, I would like to...:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Exert fine-grained control over the tracks that my radio station plays.&lt;/li&gt;
&lt;li&gt;Use my web browser to add new YouTube tracks to my radio station.
That's it; I'm a simple man, with simple needs.  I can write a small-ish web app to do these things, and give it read-write access to the /radio volume.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And here it is:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4NAt6SUv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q4kgbpowoew5lablr73a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4NAt6SUv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q4kgbpowoew5lablr73a.png" alt="MixBooth"&gt;&lt;/a&gt;&lt;br&gt;
I call it &lt;strong&gt;MixBooth&lt;/strong&gt;. It's &lt;a href="https://github.com/jhunt/mixbooth"&gt;on GitHub too&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;MixBooth is a small Vue.js front-end backed by an even smaller Go HTTP (REST) API.  The backend piece consists of only three endpoints:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This one retrieves the current playlist. The Vue bits render this on the bottom half, under the &lt;strong&gt;Current Track Line-Up&lt;/strong&gt; heading.  The checkboxes are there so that you, intrepid Radio Disk Jockey that you are, can remove tracks from the rotation, and add them back in, by way of our next endpoint:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Tired of the same song popping up?  Yank the cassette!&lt;/p&gt;

&lt;p&gt;Finally, to get new stuff &lt;em&gt;into&lt;/em&gt; the station, from YouTube, we have:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;For a deeper dive into the code, check out the &lt;a href="https://github.com/jhunt/mixbooth"&gt;GitHub repository.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To containerize this thing, we're first going to look at our Dockerfile.  Keep in mind I specifically wrote &lt;em&gt;this piece of software&lt;/em&gt; to work with &lt;em&gt;this radio station deployment&lt;/em&gt;.  I was "filling in the gaps" so to speak.  This is a long Dockerfile – the longest we've seen so far – so we'll take it in parts.&lt;/p&gt;

&lt;p&gt;Our first part is the &lt;code&gt;api&lt;/code&gt; stage.  Here, we build the Go backend application, using tooling that should be familiar to most Go programmers (even terrible ones like me):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM golang:1.15 AS api
WORKDIR /app
COPY go.mod .
COPY go.sum .
COPY main.go .
RUN go build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our next stage (in the same Dockerfile) builds the Vue component, using tooling that is recognizable to Node developers (but maybe not to the aformentioned Go rockstars):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM node:15 AS ux
WORKDIR /app
COPY ux .
RUN yarn install
RUN yarn build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, in the last stage, we'll tie it all back together, copying in assets from our build stages, with some Ubuntu packaging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:20.04
RUN apt-get update \
 &amp;amp;&amp;amp; DEBIAN_FRONTEND=noninteractive  apt-get install -y python3 python3-pip ffmpeg \
 &amp;amp;&amp;amp; pip3 install youtube-dl \
 &amp;amp;&amp;amp; apt-get remove -y python3-pip \
 &amp;amp;&amp;amp; apt-get autoremove -y \
 &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

COPY --from=api /app/mixbooth /usr/bin/mixbooth
COPY --from=ux  /app/dist     /htdocs
COPY            ingest        /usr/bin

EXPOSE 5000

ENV HTDOCS_ROOT=/htdocs
CMD ["mixbooth"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember: we need Python (and PIP!) for the &lt;code&gt;youtube-dl&lt;/code&gt; bits; this web UI literally &lt;a href="https://github.com/jhunt/mixbooth/blob/master/main.go#L165-L197"&gt;shells out to run youtube-dl&lt;/a&gt; when you ask to ingest new tracks.&lt;/p&gt;

&lt;p&gt;In fact, let's take &lt;a href="https://github.com/jhunt/mixbooth/blob/master/ingest"&gt;a closer look at that &lt;code&gt;ingest&lt;/code&gt; script&lt;/a&gt; we're copying in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
set -eu

mkdir -p /tmp/ytdl.$$
pushd /tmp/ytdl.$$
for url in "$@"; do
  youtube-dl -x "$url"
  for file in *; do
    ffmpeg -i "$file" -vn -c:a libopus "$file.opus"
    mv "$file.opus" $RADIO_ROOT/
    echo "$RADIO_ROOT/$file.opus" &amp;gt;&amp;gt; $RADIO_ROOT/playlist.m3u
  done
done
popd
rm -rf /tmp/ytdl.$$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want to point out that instead of hard-coding the radio files mountpoint to something like &lt;code&gt;/radio&lt;/code&gt;, I chose to rely on the &lt;code&gt;$RADIO_ROOT&lt;/code&gt; environment variable instead.  We'll use this in our next section, when we add the web interface container into our larger deployment.&lt;/p&gt;

&lt;h1&gt;
  
  
  Composing the Web UI
&lt;/h1&gt;

&lt;p&gt;Let's get this web interface into the mix from a Docker perspective, shall we?&lt;/p&gt;

&lt;p&gt;Here's the Compose file we ended up with from the last post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
version: '3'
services:
  icecast:
    image: filefrog/icecast2:latest
    ports:
      - '8000:8000'
    environment:
      ICECAST2_PASSWORD: whatever-you-want-it-to-be

  source:
    image: filefrog/liquidsoap:latest
    command:
      - |
        output.icecast(%opus,
          host = "icecast",
          port = 8000,
          password = "whatever-you-want-it-to-be",
          mount = "pirate-radio.opus",
          playlist.safe(reload=120,"/radio/playlist.m3u"))

    volumes:
      - $PWD/radio:/radio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's add a new &lt;code&gt;web&lt;/code&gt; service, using the published 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;---
version: '3'
services:
  # in addition to the other services, here's a new one:
  web:
    image: filefrog/mixbooth:latest
    environment:
      RADIO_ROOT:      /radio
      MIXBOOTH_STREAM: '//{host}:8000/pirate-radio.opus'
    ports:
      - 5000:5000
    volumes:
      - $PWD/radio:/radio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will spin up the (public) MixBooth image, and bind it on port 5000.  We point to the same host path (&lt;code&gt;$PWD/radio&lt;/code&gt;) as we used for the LiquidSoap container – we need them to both be looking at the &lt;em&gt;exact same files&lt;/em&gt;, so that we can add new tracks for the stream source to pick up, and modify the playlist it uses.&lt;/p&gt;

&lt;p&gt;We also added the &lt;code&gt;$RADIO_ROOT&lt;/code&gt; environment variable, since MixBooth has no preconceived notions of &lt;em&gt;where&lt;/em&gt; the audio files ought to go.  We also set the funny-looking &lt;code&gt;$MIXBOOTH_STREAM&lt;/code&gt; environment variable like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      MIXBOOTH_STREAM: '//{host}:8000/pirate-radio.opus'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is highly-specific to what MixBooth does.  The embedded player for the radio station, is little more than an HTML 5 &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element with the appropriate stream source elements.  The heavy lifting is done by the browser – thankfully!  However, much as it was clueless about where the audio tracks should live, it likewise flummoxed by where, precisely, one would go to listen to those tracks.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;$MIXBOOTH_STREAM&lt;/code&gt; environment variable encodes that information, but it does so with some late-binding templating.  More precisely, the &lt;code&gt;{host}&lt;/code&gt; bit will be replaced, &lt;em&gt;by the Javascript in the visitor's browser&lt;/em&gt;, with whatever hostname they used to access the web interface itself.  Come in by IP?  Hit the Icecast endpoint by IP.  Used a domain name and TLS?  Listen in secure comfort, oblivious to the numbers that underpin the very Internet.&lt;/p&gt;

&lt;p&gt;These were both conscious design decisions made (by me) while implementing this missing piece of the puzzle.  By abstracting the site- and station-specific configuration out of the code, and even out of the "configuration" (such as it is), I was able to make the deployment more cohesive and explicit.  Since you can't &lt;em&gt;not&lt;/em&gt; spell out precisely where the wiring goes, the resulting &lt;code&gt;docker-compose.yml&lt;/code&gt; is much easier to understand.&lt;/p&gt;

&lt;p&gt;(If you're into software engineering self-reflection, this forthrightness is aimed squarely at reducing &lt;a href="https://en.wikipedia.org/wiki/Action_at_a_distance_(computer_programming)"&gt;Action At A Distance&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Here's the final Compose file; take it for a spin and see what you think!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--------
version: '3'
services:
  icecast:
    image: filefrog/icecast2:latest
    ports:
      - '8000:8000'
    environment:
      ICECAST2_PASSWORD: whatever-you-want-it-to-be

  source:
    image: filefrog/liquidsoap:latest
    command:
      - |
        output.icecast(%opus,
          host = "icecast",
          port = 8000,
          password = "whatever-you-want-it-to-be",
          mount = "pirate-radio.opus",
          playlist.safe(reload=120,"/radio/playlist.m3u"))
    volumes:
      - $PWD/radio:/radio

  web:
    image: filefrog/mixbooth:latest
    environment:
      RADIO_ROOT:      /radio
      MIXBOOTH_STREAM: '//{host}:8000/pirate-radio.opus'
    ports:
      - 5000:5000
    volumes:
      - $PWD/radio:/radio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next time, we'll pick this whole deployment up and dump it onto the nearest &lt;a href="https://www.linode.com/products/kubernetes/"&gt;convenient Kubernetes cluster&lt;/a&gt; – stay tuned!&lt;/p&gt;

</description>
      <category>containers</category>
      <category>tutorial</category>
      <category>cloud</category>
      <category>docker</category>
    </item>
    <item>
      <title>Implementing Non-Trivial Containerized Systems - Part 3: Deploying Containers Together</title>
      <dc:creator>James Hunt</dc:creator>
      <pubDate>Fri, 11 Jun 2021 22:17:03 +0000</pubDate>
      <link>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-part-3-deploying-containers-together-3l8h</link>
      <guid>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-part-3-deploying-containers-together-3l8h</guid>
      <description>&lt;p&gt;This is the third part of a multi-part series on designing and building non-trivial containerized solutions. We're making a radio station using off-the-shelf components and some home-spun software, all on top of Docker, Docker Compose, and eventually, Kubernetes.&lt;/p&gt;

&lt;p&gt;In this part, we're going to take the notes we took during the &lt;a href="https://starkandwayne.com/blog/implementing-non-trivial-containerized-systems-part-2-containerizing-with-docker/"&gt;&lt;em&gt;last part&lt;/em&gt;&lt;/a&gt; (you did take notes, didn't you?) and distill them down into some infrastructure-as-code. It all starts with Docker Compose.&lt;/p&gt;

&lt;p&gt;Welcome back! If you've been following along, you should now have a directory of .opus audio files and two containers streaming audio out to anyone who connects. If you haven't been following along, go read [the] [other parts] [first]. Then come back.&lt;/p&gt;

&lt;p&gt;The Dockerfiles we have made so far are eminently portable. Anyone can take those little recipes and rebuild our container images, more or less. The images themselves are also portable – we could &lt;code&gt;docker push&lt;/code&gt; them up to DockerHub and let the world enjoy their container-y goodness.&lt;/p&gt;

&lt;p&gt;What's not so portable is how we run and wire those containers up. That part is actually quite proprietary, dense, and easily forgotten. Let's rectify that. Let's use Docker Compose.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--45MEQXSI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0a1m1uuet7c2erywjiz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--45MEQXSI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0a1m1uuet7c2erywjiz6.png" alt="Docker-Compose"&gt;&lt;/a&gt;&lt;br&gt;
Docker Compose is a small-ish orchestration system for a single Docker host. It lets us specify how we want our containers run; what ports to forward, what volumes to mount, etc. It also has a few tricks up its sleeve that will make our lives better. Best of all, Docker Compose recipes can be shared with others who also like building bespoke Internet radio stations. We are legion. And we use Docker.&lt;/p&gt;

&lt;p&gt;What's in a recipe? It starts, as most things in the container world do, with a YAML file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat docker-compose.yml
---
version: '3'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll be using &lt;a href="https://docs.docker.com/compose/compose-file/compose-file-v3/"&gt;version 3 of the Compose specification&lt;/a&gt;. That's not terribly important right now, but there are certain concepts that won't work if we're not on a new enough version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you have been following along, you've probably got containers still running from our previous experiments. Luckily, we specified the &lt;code&gt;--rm&lt;/code&gt; flag when we ran these containers, so all we need to do to clean up is stop them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker stop icecast
$ docker stop liquid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker Compose deals in containers, but it does so in scalable sets of identical containers that it calls "services". For our immediate purposes, we won't be scaling these things out, so a service &lt;em&gt;is&lt;/em&gt; effectively a container, and vice versa.&lt;/p&gt;

&lt;p&gt;We'll start by adding our first service / container: Icecast2 itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
version: '3'
services:
  icecast:
    image: filefrog/icecast2:latest
    ports:
      - '8000:8000'
    environment:
      ICECAST2_PASSWORD: whatever-you-want-it-to-be
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essentially, we're taking the &lt;code&gt;docker run&lt;/code&gt; command and committing it to a file, in YAMLese. To get this container running, we'll use the docker-compose command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts the containers up in "foreground mode" - their output will be multiplexed onto your terminal. As we add more containers (err... services), Docker Compose will start color-coding their output to help us keep them separate.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hP55hyCr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0or2ojv7ptvh4kuxhj1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hP55hyCr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0or2ojv7ptvh4kuxhj1t.png" alt="docker-compose-up"&gt;&lt;/a&gt;&lt;br&gt;
To get your shell prompt back, terminate the foreground process with a Ctrl-C.&lt;/p&gt;

&lt;p&gt;That was so easy, let's add our LiquidSoap service / container into the mix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--------
version: '3'
services:
  icecast:
    image: filefrog/icecast2:latest
    ports:
      - '8000:8000'
    environment:
      ICECAST2_PASSWORD: whatever-you-want-it-to-be

  source:
    image: filefrog/liquidsoap:latest
    command:
      - |
        output.icecast(%opus,
          host = "10.128.0.56",
          port = 8000,
          password = "whatever-you-want-it-to-be",
          mount = "pirate-radio.opus",
          playlist.safe(reload=120,"/radio/playlist.m3u"))

    volumes:
      - $PWD/radio:/radio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, we're going to give docker-compose a -d flag, so that it forks into the foreground and keeps on running, while we get our shell prompt back to get EVEN MORE WORK DONE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Docker Compose is just creating containers using Docker, we can use the docker CLI to do all the things we're already accustomed to. For example, to see what LiquidSoap is up to, we can check the docker logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker logs radio_source_1
docker logs radio_source_1
2021/03/30 13:16:36 &amp;gt;&amp;gt;&amp;gt; LOG START
2021/03/30 13:16:36 [main:3] Liquidsoap 1.4.4
2021/03/30 13:16:36 [main:3] Using: bytes=[distributed with OCaml 4.02 or above] pcre=7.4.6 sedlex=2.3 menhirLib=20201216 dtools=0.4.1 duppy=0.8.0 cry=0.6.4 mm=0.5.0 xmlplaylist=0.1.4 lastfm=0.3.2 ogg=0.5.2 vorbis=0.7.1 opus=0.1.3 speex=0.2.1 mad=0.4.6 flac=0.1.5 flac.ogg=0.1.5 dynlink=[distributed with Ocaml] lame=0.3.4 shine=0.2.1 gstreamer=0.3.0 frei0r=0.1.1 fdkaac=0.3.2 theora=0.3.1 ffmpeg=0.4.3 bjack=0.1.5 alsa=0.2.3 ao=0.2.1 samplerate=0.1.4 taglib=0.3.6 ssl=0.5.9 magic=0.7.3 camomile=1.0.2 inotify=2.3 yojson=1.7.0 faad=0.4.0 soundtouch=0.1.8 portaudio=0.2.1 pulseaudio=0.1.3 ladspa=0.1.5 dssi=0.1.2 camlimages=4.2.6 srt.types=0.1.1 srt.stubs=0.1.1 srt.stubs=0.1.1 srt=0.1.1 lo=0.1.2 gd=1.0a5
2021/03/30 13:16:36 [gstreamer.loader:3] Loaded GStreamer 1.16.2 0
2021/03/30 13:16:36 [frame:3] Using 44100Hz audio, 25Hz video, 44100Hz master.
... etc ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was fairly painless. All of the things we can express in terms of a &lt;code&gt;docker run&lt;/code&gt; command can be specified, in YAML (of course), for Docker Compose. This means we'll never get to the point of wanting to "wrap up" all the Docker stuff we've been playing with into something more serious.&lt;/p&gt;

&lt;p&gt;There are a few things Docker Compose gives us that we don't (easily) get with pure docker run: a bit of DNS. Recall from the last post that when we told LiquidSoap where to stream to, we had to use the actual IP of the Docker host. With Docker Compose, our containers all live on the same virtual bridge, and we get free DNS for connecting them together. From the container, the name &lt;code&gt;icecast&lt;/code&gt; will resolved to the internal &lt;em&gt;bridge&lt;/em&gt; IP address of the &lt;code&gt;icecast&lt;/code&gt; containers. This means we can improve our compose recipe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--------
version: '3'
services:
  icecast:
    image: filefrog/icecast2:latest
    ports:
      - '8000:8000'
    environment:
      ICECAST2_PASSWORD: whatever-you-want-it-to-be

  source:
    image: filefrog/liquidsoap:latest
    command:
      - |
        output.icecast(%opus,
          host = "icecast",
          port = 8000,
          password = "whatever-you-want-it-to-be",
          mount = "pirate-radio.opus",
          playlist.safe(reload=120,"/radio/playlist.m3u"))

    volumes:
      - $PWD/radio:/radio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This final version of our recipe is &lt;em&gt;completely&lt;/em&gt; portable; you don't have to adhere to my personal network numbering scheme to make use of this deployment asset – just download it and compose it up!&lt;/p&gt;

&lt;p&gt;Before I let you go, we should verify that everything is still working. The Icecast web interface should still load, and you should be able to listen to those two goofballs on &lt;a href="https://rentbuybuild.cloud/"&gt;Rent / Buy / Build (the podcast)&lt;/a&gt; talk about how to source the components of your Cloud-Native platform.&lt;/p&gt;

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

</description>
      <category>containers</category>
      <category>tutorial</category>
      <category>cloud</category>
      <category>docker</category>
    </item>
    <item>
      <title>Implementing Non-Trivial Containerized Systems - Part 2: Containerizing With Docker</title>
      <dc:creator>James Hunt</dc:creator>
      <pubDate>Fri, 04 Jun 2021 17:45:18 +0000</pubDate>
      <link>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-part-2-containerizing-with-docker-5e5k</link>
      <guid>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-part-2-containerizing-with-docker-5e5k</guid>
      <description>&lt;p&gt;This is the second part of a multi-part series on designing and building non-trivial containerized solutions. We're making a radio station using off-the-shelf components and some home-spun software, all on top of Docker, Docker Compose, and eventually, Kubernetes.&lt;/p&gt;

&lt;p&gt;In this part, we're going to take the architecture we cooked up in the last part, and see if we can't get it running in containers, using nothing but Docker itself.&lt;br&gt;
Today, we're going to build a radio station. We drew up the architecture in &lt;a href="https://starkandwayne.com/blog/implementing-non-trivial-containerized-systems-part-1-picking-components/"&gt;&lt;em&gt;our last post&lt;/em&gt;&lt;/a&gt;:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--u5UBA6_2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oebipl4fmwdjyk02nrpz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--u5UBA6_2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oebipl4fmwdjyk02nrpz.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
Roll up your sleeves, we're gonna start building some containers.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sourcing Audio - youtube-dl
&lt;/h2&gt;

&lt;p&gt;Before we can broadcast audio, we have to have audio, and we're going to get our audio from YouTube. This, then, is our first container.&lt;/p&gt;

&lt;p&gt;We're going to start with Ubuntu 20.04 (Focal Fossa) base image. I like this image not only because it's familiar and homey; but also for access to &lt;code&gt;apt&lt;/code&gt; for package management, and (more importantly) all those sweet, sweet Debian packages. Case in point, we need Python (youtube-dl is written in Python):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:20.04
RUN apt-get update \
 &amp;amp;&amp;amp; apt-get install -y python3 python3-pip \
 &amp;amp;&amp;amp; pip3 install youtube-dl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're going to set up an &lt;code&gt;ENTRYPOINT&lt;/code&gt; in our image, so that when we &lt;code&gt;docker run&lt;/code&gt; it, we can pass it arguments, and use the container as if it were youtube-dl itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:20.04
RUN apt-get update \
 &amp;amp;&amp;amp; apt-get install -y python3 python3-pip \
 &amp;amp;&amp;amp; pip3 install youtube-dl

WORKDIR /radio
ENTRYPOINT ["youtube-dl"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's run what we've got so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker build -t youtube-dl .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM ubuntu:20.04
 ---&amp;gt; f63181f19b2f
Step 2/2 : RUN apt-get update  &amp;amp;&amp;amp; apt-get install -y python3 python3-pip  &amp;amp;&amp;amp; pip3 install youtube-dl
 ---&amp;gt; Running in cbc3f65f6028
&amp;lt;a whole bunch of apt-get output...&amp;gt;
Collecting youtube-dl
  Downloading youtube_dl-2021.3.14-py2.py3-none-any.whl (1.9 MB)
Installing collected packages: youtube-dl
Successfully installed youtube-dl-2021.3.14
Removing intermediate container cbc3f65f6028
 ---&amp;gt; 66648c847e34
Successfully built 66648c847e34
Successfully tagged youtube-dl:latest

$ docker run youtube-dl --version
2021.03.14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we used an &lt;code&gt;ENTRYPOINT&lt;/code&gt;, the &lt;code&gt;--version&lt;/code&gt; argument we gave to the container got passed through youtube-dl. This is important, since this will be our primary means of interacting with youtube-dl.&lt;/p&gt;

&lt;p&gt;Let's try to download some back episodes of my podcast, &lt;strong&gt;Rent / Buy / Build&lt;/strong&gt;. You can find those &lt;a href="https://www.youtube.com/watch?v=D7hxDYBoHvQ&amp;amp;list=PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8"&gt;&lt;em&gt;here&lt;/em&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run youtube-dl https://youtu.be/D7hxDYBoHvQ?list=PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8
[youtube:tab] Downloading playlist PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8 - add --no-playlist to just download video D7hxDYBoHvQ
[youtube:tab] PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8: Downloading webpage
[youtube:tab] PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8: Downloading webpage
[download] Downloading playlist: Rent / Buy / Build Episodes
[youtube:tab] playlist Rent / Buy / Build Episodes: Downloading 1 videos
[download] Downloading video 1 of 1
[youtube] D7hxDYBoHvQ: Downloading webpage
[youtube] D7hxDYBoHvQ: Downloading MPD manifest
[download] Destination: Episode 0 - What's All This Then-D7hxDYBoHvQ.mp4
[download] 100% of 12.76MiB in 00:0296MiB/s ETA 00:003
[download] Finished downloading playlist: Rent / Buy / Build Episodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks like we got the file, but where did it go? Into the ether. When our container process (youtube-dl) exits (i.e. finishes downloading and transcoding the video), the root filesystem for the container is wiped away. This is a feature, honest. If we want to keep any of the files we create in the container, we're going to need to bind-mount in some persistent(-ish) storage. If you recall, we set our &lt;code&gt;WORKDIR&lt;/code&gt; to &lt;code&gt;/radio&lt;/code&gt;, so if we can mount a directory from &lt;em&gt;outside&lt;/em&gt; of the container, we can keep those files around longer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mkdir radio
$ docker run -v $PWD/radio:/radio youtube-dl https://youtu.be/D7hxDYBoHvQ?list=PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8
[youtube:tab] Downloading playlist PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8 - add --no-playlist to just download video D7hxDYBoHvQ
[youtube:tab] PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8: Downloading webpage
[youtube:tab] PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8: Downloading webpage
[download] Downloading playlist: Rent / Buy / Build Episodes
[youtube:tab] playlist Rent / Buy / Build Episodes: Downloading 1 videos
[download] Downloading video 1 of 1
[youtube] D7hxDYBoHvQ: Downloading webpage
[youtube] D7hxDYBoHvQ: Downloading MPD manifest
[download] Destination: Episode 0 - What's All This Then-D7hxDYBoHvQ.mp4
[download] 100% of 12.76MiB in 00:0218MiB/s ETA 00:004
[download] Finished downloading playlist: Rent / Buy / Build Episodes

$ ls -l radio
total 13312
-rw-r--r-- 1 jhunt staff 13383936 Mar 22 14:30 "Episode 0 - What's All This Then-D7hxDYBoHvQ.mp4"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yay! Files! Too bad they're video files, not audio files. To fix that, let's throw ffmpeg into our container image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:20.04
RUN apt-get update \
 &amp;amp;&amp;amp; DEBIAN_FRONTEND=noninteractive apt-get install -y python3 python3-pip ffmpeg \
 &amp;amp;&amp;amp; pip3 install youtube-dl

WORKDIR /radio
ENTRYPOINT ["youtube-dl"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Note: I had to add that &lt;code&gt;DEBIAN_FRONTEND=noninteractive&lt;/code&gt; bit to keep the package manager from bothering me with terminal prompts about timezone data.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With ffmpeg, we can take the files that come out of youtube-dl, drop the video components, and convert the audio into a common format (which will be useful when we get to LiquidSoap programming).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run -v $PWD/radio:/radio --entrypoint ffmpeg youtube-dl \
  -i "Episode 0 - What's All This Then-D7hxDYBoHvQ.mp4" \
  -vn -c:a libopus \
  "Episode 0 - What's All This Then-D7hxDYBoHvQ.opus"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a fair bit of domain expertise regarding file formats and conversion, and it's all right out there, in the open. It's also a two step process. This is a problem that can be fixed by a small shell script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh
set -eu

mkdir -p /tmp/ytdl.$$
cd /tmp/ytdl.$$
for url in "$@"; do
  youtube-dl -x "$url"
  for f in *; do
    ffmpeg -i "$f" -vn -c:a libopus "$f.opus"
    rm "$f"
    mv "$f.opus" /radio
  done
done
cd /
rm -rf /tmp/ytdl.$$
ls -1 /radio/*.opus &amp;gt; /radio/playlist.m3u
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one new feature in this shell script: &lt;strong&gt;playlist management&lt;/strong&gt;. After we've ingested new files into &lt;code&gt;/radio&lt;/code&gt;, we rebuild a &lt;code&gt;.m3u&lt;/code&gt; playlist. This is a simple file: one file path per line, read sequentially. Piping the &lt;code&gt;ls&lt;/code&gt; output through &lt;code&gt;sort&lt;/code&gt; ensures that we have a stable playlist order; we'll let other systems randomize playlist ordering if they wish.&lt;/p&gt;

&lt;p&gt;We can copy that script (which I've unimaginatively named &lt;code&gt;entrypoint&lt;/code&gt;) into our docker image via the Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:20.04
RUN apt-get update \
 &amp;amp;&amp;amp; DEBIAN_FRONTEND=noninteractive apt-get install -y python3 python3-pip ffmpeg \
 &amp;amp;&amp;amp; pip install youtube-dl

WORKDIR /radio

COPY entrypoint /usr/bin/entrypoint
RUN chmod 0755 /usr/bin/entrypoint
ENTRYPOINT ["entrypoint"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this change in place, we go back to invoking our container (after a fresh build!) with just the URL we want downloaded and transcoded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run -v $PWD/radio:/radio youtube-dl https://youtu.be/D7hxDYBoHvQ?list=PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8
[youtube:tab] Downloading playlist PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8 - add --no-playlist to just download video D7hxDYBoHvQ
[youtube:tab] PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8: Downloading webpage
[youtube:tab] PLGDWYlG32yKoYAuREw9eSLFIdWJZ_3rW8: Downloading webpage
[download] Downloading playlist: Rent / Buy / Build Episodes
[youtube:tab] playlist Rent / Buy / Build Episodes: Downloading 1 videos
[download] Downloading video 1 of 1
[youtube] D7hxDYBoHvQ: Downloading webpage
[youtube] D7hxDYBoHvQ: Downloading MPD manifest
[download] Destination: Episode 0 - What's All This Then-D7hxDYBoHvQ.m4a
[download] 100% of 9.73MiB in 00:0123MiB/s ETA 00:005
[ffmpeg] Correcting container in "Episode 0 - What's All This Then-D7hxDYBoHvQ.m4a"
[ffmpeg] Post-process file Episode 0 - What's All This Then-D7hxDYBoHvQ.m4a exists, skipping
[download] Finished downloading playlist: Rent / Buy / Build Episodes
ffmpeg version 4.2.4-1ubuntu0.1 Copyright (c) 2000-2020 the FFmpeg developers
  built with gcc 9 (Ubuntu 9.3.0-10ubuntu2)
  configuration: --prefix=/usr --extra-version=1ubuntu0.1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-nvenc --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
  libavutil      56. 31.100 / 56. 31.100
  libavcodec     58. 54.100 / 58. 54.100
  libavformat    58. 29.100 / 58. 29.100
  libavdevice    58.  8.100 / 58.  8.100
  libavfilter     7. 57.100 /  7. 57.100
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  5.100 /  5.  5.100
  libswresample   3.  5.100 /  3.  5.100
  libpostproc    55.  5.100 / 55.  5.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'Episode 0 - What's All This Then-D7hxDYBoHvQ.m4a':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2mp41
    encoder         : Lavf58.29.100
  Duration: 00:10:30.10, start: 0.000000, bitrate: 129 kb/s
    Stream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 128 kb/s (default)
    Metadata:
      handler_name    : ISO Media file produced by Google Inc.
Stream mapping:
  Stream #0:0 -&amp;gt; #0:0 (aac (native) -&amp;gt; opus (libopus))
Press [q] to stop, [?] for help
[libopus @ 0x560757a9e280] No bit rate set. Defaulting to 96000 bps.
Output #0, opus, to 'Episode 0 - What's All This Then-D7hxDYBoHvQ.m4a.opus':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2mp41
    encoder         : Lavf58.29.100
    Stream #0:0(eng): Audio: opus (libopus), 48000 Hz, stereo, flt, 96 kb/s (default)
    Metadata:
      handler_name    : ISO Media file produced by Google Inc.
      encoder         : Lavc58.54.100 libopus
      major_brand     : isom
      minor_version   : 512
      compatible_brands: isomiso2mp41
size=    6139kB time=00:10:30.11 bitrate=  79.8kbits/s speed=72.5x
video:0kB audio:6085kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.876994%

$ ls -l radio
total 6144
-rw-r--r-- 1 jhunt staff 6286163 Mar 22 14:52 "Episode 0 - What's All This Then-D7hxDYBoHvQ.m4a.opus"
-rw-r--r-- 1 jhunt staff      61 Mar 22 14:52  playlist.m3u
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can even do multiple!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run -v $PWD/radio:/radio youtube-dl \
  ... \
  ... \
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  You're On The Air With Icecast
&lt;/h2&gt;

&lt;p&gt;Time to turn up the transmitters and start broadcasting! For that, we need Icecast. As before, we'll start with an Ubuntu 20.04 base and add the packages we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:20.04
RUN apt-get update \
 &amp;amp;&amp;amp; apt-get install -y icecast2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Icecast won't let just anybody stream music to it. That would be pure chaos! This is a &lt;em&gt;broadcast&lt;/em&gt; radio station, not a peer-to-peer free-for-all. We chose the tunes. We pick the commercials. We write the jingles. To keep out the riff-raff, Icecast uses speakeasy authentication. Each source driver is required to provide the secret password before we'll accept its audio stream.&lt;/p&gt;

&lt;p&gt;The Icecast core team definitely has a sense of humor, because in the default configuration, the source password is "hackme". We need to change that.&lt;/p&gt;

&lt;p&gt;It's important to remember that Icecast was born when XML was all the rage and came of age before it was fashionable to configure things via environment variables. To change the source driver password, we need to modify the configuration file: &lt;code&gt;/etc/icecast2/icecast.xml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:20.04
ARG PASSWORD
RUN apt-get update \
 &amp;amp;&amp;amp; apt-get install -y icecast2 \
 &amp;amp;&amp;amp; sed -i -e "s/&amp;gt;hackme&amp;lt;/&amp;gt;$PASSWORD&amp;lt;/" /etc/icecast2/icecast.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sed command finds all instances of &lt;code&gt;"&amp;gt;hackme&amp;lt;"&lt;/code&gt; and replaces it with &lt;code&gt;"&amp;gt;$PASSWORD&amp;lt;"&lt;/code&gt;. The new &lt;code&gt;ARG&lt;/code&gt; directive sets up a Docker &lt;em&gt;build argument&lt;/em&gt;, which is a bit like an environment variable that only exists when the container image is &lt;em&gt;built&lt;/em&gt;. We specify it thusly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker build --build-arg PASSWORD=sekrit -t icecast .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take it for a spin and make sure everything is ship shape.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run icecast icecast2 -c /etc/icecast2/icecast.xml
[2021-03-22  19:04:15] WARN CONFIG/_parse_root Warning,  not configured, using default value "localhost". This will cause problems, e.g. with YP directory listings.
[2021-03-22  19:04:15] WARN CONFIG/_parse_root Warning,  not configured, using default value "Earth".
[2021-03-22  19:04:15] WARN CONFIG/_parse_root Warning,  contact not configured, using default value "icemaster@localhost".
[2021-03-22  19:04:15] WARN fserve/fserve_recheck_mime_types Cannot open mime types file /etc/mime.types
ERROR: You should not run icecast2 as root
Use the changeowner directive in the config file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drat.&lt;/p&gt;

&lt;p&gt;Icecast doesn't want to run as root, so we're going to need to provision a user account &lt;em&gt;inside&lt;/em&gt; the container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:20.04
ARG PASSWORD
RUN apt-get update \
 &amp;amp;&amp;amp; apt-get install -y icecast2 \
 &amp;amp;&amp;amp; sed -i -e "s/&amp;gt;hackme&amp;lt;/&amp;gt;$PASSWORD&amp;lt;/" /etc/icecast2/icecast.xml \
 &amp;amp;&amp;amp; useradd radio \
 &amp;amp;&amp;amp; chown -R radio:radio /etc/icecast2 /var/log/icecast2
USER radio
ENTRYPOINT ["icecast2"]
CMD ["-c", "/etc/icecast2/icecast.xml"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;useradd&lt;/code&gt; command updates the files inside the container (notably, &lt;code&gt;/etc/passwd&lt;/code&gt;), while the &lt;code&gt;USER&lt;/code&gt; directive tells Docker that it should switch to that effective UID when the container starts up.&lt;/p&gt;

&lt;p&gt;I also took the liberty of setting an entrypoint and some arguments. When you specify both of them, you get the ability to provide "default" arguments (CMD) to the base command of the docker image (ENTRYPOINT).&lt;/p&gt;

&lt;p&gt;That's all there is to the Icecast image. Running it requires a little bit more effort, but not much. Notably, we need to provide a port so that listeners can find the radio station, and so that source drivers can stream in their audio. We do this by forwarding ports from the Docker host &lt;em&gt;into&lt;/em&gt; the container, 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;$ docker run -p 8000:8000 icecast
[2021-03-22  19:11:48] WARN CONFIG/_parse_root Warning,  not configured, using default value "localhost". This will cause problems, e.g. with YP directory listings.
[2021-03-22  19:11:48] WARN CONFIG/_parse_root Warning,  not configured, using default value "Earth".
[2021-03-22  19:11:48] WARN CONFIG/_parse_root Warning,  contact not configured, using default value "icemaster@localhost".
[2021-03-22  19:11:48] WARN fserve/fserve_recheck_mime_types Cannot open mime types file /etc/mime.types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the container namespaces, Icecast is binding all interfaces on port 80. Outside, Docker (with a little help from &lt;code&gt;iptables&lt;/code&gt;) is forwarding traffic bound for the &lt;em&gt;host&lt;/em&gt; on port 8000 into the container, on port 8000. (Incidentally, this is &lt;em&gt;usually&lt;/em&gt; done via a Linux bridge and two ends of a &lt;em&gt;veth&lt;/em&gt; pair, but that's a topic for a different, more nerdy post.)&lt;/p&gt;

&lt;p&gt;You can see the port assignments in both the docker ps output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED        STATUS                  PORTS                  NAMES
52993e74a1b3   icecast   "icecast2 -c /etc/ic…"   1 second ago   Up Less than a second   0.0.0.0:8000-&amp;gt;8000/tcp   friendly_albattani
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and also via netstat or lsof (pick your favorite!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# lsof -nP -i TCP
com.docke 29389 jhunt   41u     IPv6  0xb3ce8809472e12f         0t0                 TCP *:8000 (LISTEN)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's leave Icecast to its thoughts, and move onto setting up our audio source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Magic That Is LiquidSoap
&lt;/h2&gt;

&lt;p&gt;What is &lt;a href="https://www.liquidsoap.info/"&gt;LiquidSoap&lt;/a&gt;? MAGIC.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PxiTpCBF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oam6fwbrats63fwb31m5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PxiTpCBF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oam6fwbrats63fwb31m5.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
I truly, and fervently mean that. Their website is more modest:&lt;/p&gt;

&lt;p&gt;LiquidSoap is a powerful and flexible language for describing audio and video streams. It offers a rich collection of operators that you can combine at will, giving you more power than you need for creating or transforming streams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;more power than you need&lt;/strong&gt; – that about sums up what we're going to use it for. We have audio files that we ripped right out of some Internet videos, and we have a radio broadcasting engine just waiting to be fed some juicy audio streams. We need a way to hook those up. And we're going to use LiquidSoap.&lt;/p&gt;

&lt;p&gt;Here's the humble .liq script we're going to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;output.icecast(%opus,
  host = "10.128.0.56",
  port = 8000,
  password = "sekrit",
  mount = "pirate-radio.opus",
  playlist.safe(reload=120,"/radio/playlist.m3u"))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But before we get ahead of ourselves, let's build out our Docker image for running that little gem.&lt;/p&gt;

&lt;p&gt;You know the drill; start with Ubuntu and add the packages you need. As we've done for every image so far, we're going to set an &lt;code&gt;ENTRYPOINT&lt;/code&gt; so that we can pretend our container &lt;em&gt;is&lt;/em&gt; the &lt;code&gt;liquidsoap&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:20.04
RUN apt-get update \
 &amp;amp;&amp;amp; apt-get install -y liquidsoap
USER nobody
ENTRYPOINT ["liquidsoap"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As with Icecast, we set a &lt;code&gt;USER&lt;/code&gt;, so that Liquidsoap doesn't get angry about us being root and all. This time, however, we're just going to use the default &lt;code&gt;nobody&lt;/code&gt; account that comes with Ubuntu.&lt;/p&gt;

&lt;p&gt;Convinced that the image is fit and fine, we can revisit that &lt;code&gt;.liq&lt;/code&gt; script. Here's a version with some more comments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# radio.liq
#
# This script streams audio from files on-disk,
# in an m3u playlist, to our Icecast server in
# opus format.
#
output.icecast(%opus,
  host = "10.128.0.56",        # where is Icecast?
  port = 8000,                 # what port is it bound on?
  password = "sekrit",         # what's the secret pass phrase?
  mount = "pirate-radio.opus", # what should we call this stream?

  # create an infallible playlist (hence 'safe')
  # using the files on-disk, in /radio.
  #
  playlist.safe(reload=120,"/radio/playlist.m3u"))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Remember, LiquidSoap is a fully-fledged programming language. You can do all sorts of crazy things with it.)&lt;/p&gt;

&lt;p&gt;Notably, we're going to output a stream to our Icecast server. Because the container has it's own network stack (containers!) I've specified the IP of the Docker host &lt;em&gt;itself&lt;/em&gt; as the Icecast host endpoint. This causes packets to route out of the container and eventually find their way through our port-forwarding rules and into the other container. In the next part of this series, when we bring Docker Compose into the picture, we'll sand that particular rough patch down sufficiently.&lt;/p&gt;

&lt;p&gt;Remember that &lt;em&gt;m3u&lt;/em&gt; playlist we made while we were ingesting new audio files via youtube-dl? That's what this LiquidSoap script is going to consume while it builds its audio stream. I want to point out something that may not be obvious at first: the paths specified &lt;em&gt;in&lt;/em&gt; the playlist must be valid when our script goes to consume them. Keeping everything always mounted at &lt;code&gt;/radio&lt;/code&gt; suffices, but it's not the only way.&lt;/p&gt;

&lt;p&gt;This is it, the final piece of the puzzle. Let's spin up that docker container and see if it works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker run -d --rm --name icecast -p 8000:8000 icecast
$ docker run --name liquid -v $PWD/radio:/radio 'output.icecast(%opus, host=...'
...
2021/03/22 20:09:17 [playlist(dot)m3u:3] Loading playlist...
2021/03/22 20:09:17 [playlist(dot)m3u:3] No mime type specified, trying autodetection.
2021/03/22 20:09:17 [playlist(dot)m3u:3] Playlist treated as format application/x-mpegURL
2021/03/22 20:09:17 [decoder:3] Method "OGG" accepted "/radio/Episode 0 - What's All This Then-D7hxDYBoHvQ.m4a.opus".
2021/03/22 20:09:17 [playlist(dot)m3u:3] Successfully loaded a playlist of 1 tracks.
2021/03/22 20:09:17 [decoder:3] Method "OGG" accepted "/radio/Episode 0 - What's All This Then-D7hxDYBoHvQ.m4a.opus".
2021/03/22 20:09:17 [playlist(dot)m3u:3] Prepared "/radio/Episode 0 - What's All This Then-D7hxDYBoHvQ.m4a.opus" (RID 2).
2021/03/22 20:09:17 [pirate-radio(dot)opus:3] Connecting mount pirate-radio.opus for source@192.168.88.225...
2021/03/22 20:09:17 [pirate-radio(dot)opus:3] Connection setup was successful.
2021/03/22 20:09:17 [clock.wallclock_main:3] Streaming loop starts, synchronized with wallclock.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs for the LiquidSoap container clearly show that the source was able to connect and start streaming in audio. Now we can visit the web interface (http://:8000), which should look something like this:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_-6zOW8B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lq5eew67hyyr7cdoqe2y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_-6zOW8B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lq5eew67hyyr7cdoqe2y.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
Congratulations. You're officially an Internet radio DJ. Now, click that play button, enjoy the show, and go brainstorm a better radio handle.&lt;/p&gt;

&lt;p&gt;In the next part of the series, we'll gather up all of the port forwarding rules, commands, and volume bind-mounts, and wrap it all up in a neat little package using Docker Compose. This will make our lives easier as we iterate on the whole system.&lt;/p&gt;

</description>
      <category>containers</category>
      <category>tutorial</category>
      <category>cloud</category>
      <category>docker</category>
    </item>
    <item>
      <title>
Implementing Non-Trivial Containerized Systems - Part 1: Picking Components</title>
      <dc:creator>James Hunt</dc:creator>
      <pubDate>Thu, 06 May 2021 05:18:56 +0000</pubDate>
      <link>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-part-1-picking-components-8h8</link>
      <guid>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-part-1-picking-components-8h8</guid>
      <description>&lt;p&gt;So, you want to start a radio station, eh?&lt;/p&gt;

&lt;p&gt;This is the first part of a multi-part series on designing and building non-trivial containerized solutions. We're making a radio station using off-the-shelf components and some home-spun software, all on top of Docker, Docker Compose, and eventually, Kubernetes.&lt;/p&gt;

&lt;p&gt;In this part, we're going to explore how the different parts of the system interface with one another, to set the stage for our next post, where we Dockerize everything!&lt;/p&gt;

&lt;p&gt;I first met Icecast (&lt;a href="https://icecast.org/"&gt;https://icecast.org/&lt;/a&gt;) when I worked at a web-hosting startup around the turn of the millennium. One night, one of my co-workers and I had the crazy idea to load a bunch of audio files on the networked file server and stream them to our workstations. We could listen to music while we worked 90+ hours a week. Strange times. After realizing it wasn't as simple as exporting &lt;code&gt;.ogg&lt;/code&gt; files over HTTP, we found Icecast (and its pal, Ices2) and built a rudimentary, local-network broadcast radio station.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oNenUsCq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ephdd3drq5yrorfbu0vz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oNenUsCq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ephdd3drq5yrorfbu0vz.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
That's sort of what Icecast does – it makes streams of audio available to clients (er, &lt;em&gt;listeners&lt;/em&gt;) over HTTP. These streams are analogous to old-school AM/FM radio stations. The listener can't pick what gets played (not directly, at least), and they can't control where they are in the stream. Wherever they connect, that's what they listen to.&lt;/p&gt;

&lt;p&gt;Icecast is actually more of a go-between. It will keep track of which streams exist, and connect listeners to those streams, but it is not responsible for what's &lt;em&gt;in&lt;/em&gt; the streams. For that, you need a &lt;em&gt;source&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;em&gt;source&lt;/em&gt; can be just about anything; a physical audio input device (i.e. a microphone), a CD player, audio files, you name it. The go-to source driver for Icecast is &lt;em&gt;Ices&lt;/em&gt;, but we're going to use something with a bit more functionality; a little something called &lt;em&gt;LiquidSoap&lt;/em&gt;.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hA4zSKNe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/py0emx1zhl49zihb41c2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hA4zSKNe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/py0emx1zhl49zihb41c2.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
LiquidSoap (&lt;a href="https://www.liquidsoap.info/"&gt;https://www.liquidsoap.info/&lt;/a&gt;) is really cool. Like, &lt;strong&gt;&lt;em&gt;phenomenally&lt;/em&gt;&lt;/strong&gt; powerful. We're going to use it to combine multiple audio files into a single stream and send that off to Icecast. This doesn't even &lt;em&gt;begin&lt;/em&gt; to scratch the surface of what LiquidSoap is capable of. I hope it isn't insulted.&lt;/p&gt;

&lt;p&gt;Finally, we need to talk about where we are going to get the actual audio that we want LiquidSoap to bundle up and stream to Icecast. If I were a good teacher, this paragraph would end with a link to an S3 bucket or a GitHub project or some internet endpoint where you can download test files to play with, and we could move on.&lt;/p&gt;

&lt;p&gt;I'm not a good teacher. You're going to work for that audio.&lt;/p&gt;

&lt;p&gt;We're actually going to build a means of acquiring audio &lt;em&gt;into&lt;/em&gt; the system itself, using YouTube as our primary source. For that, we'll use a handy little tool called youtube-dl, in concert with ffmpeg, another sharp little tool that makes quick work of audio video files. Here's how it will work:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sRSMWjPE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56hnwoeyus7zx5k1isd6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sRSMWjPE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56hnwoeyus7zx5k1isd6.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
We start with a YouTube(dot)com video URL, which we will feed to youtube-dl. From that, we get a video file, on-disk. Since we're only interested in the audio track, we'll feed that video file into ffmpeg to rip out just the audio track into a more appropriate audio file format (like opus / ogg).&lt;/p&gt;

&lt;p&gt;We have arrived at our final architecture.&lt;/p&gt;

&lt;p&gt;Next up, we'll start putting things into containers, and see if our design will actually work.&lt;/p&gt;

</description>
      <category>containers</category>
      <category>tutorial</category>
      <category>cloud</category>
      <category>docker</category>
    </item>
    <item>
      <title>Implementing Non-Trivial Containerized Systems</title>
      <dc:creator>James Hunt</dc:creator>
      <pubDate>Sat, 24 Apr 2021 01:59:46 +0000</pubDate>
      <link>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-1eln</link>
      <guid>https://dev.to/jhunt/implementing-non-trivial-containerized-systems-1eln</guid>
      <description>&lt;p&gt;Your average blog post / tutorial / video about containerizing software goes a little something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/DgZIDCjCtrk" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.ytimg.com%2Fan_webp%2FDgZIDCjCtrk%2Fmqdefault_6s.webp%3Fdu%3D3000%26sqp%3DCOWBjYQG%26rs%3DAOn4CLC6ktyxf7d7ovm8j6j36HEyO-61tw"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Trouble is, all that "simple Docker stuff" is where things can (and often do!) go horribly horribly wrong, and there is precious little material available out there on the Internet for helping you through those precarious bits. No more!&lt;/p&gt;

&lt;p&gt;In this series, I'm going to take you through the complete containerization effort, using a nontrivial application composed of both off-the-shelf components and some home-spun software.&lt;/p&gt;

&lt;p&gt;Together, we're going to build and containerize an Internet radio station. Let's meet the software we're going to be using "off the shelf" to do most of the heavy lifting:&lt;/p&gt;

&lt;p&gt;Icecast2 - &lt;a href="https://icecast.org/" rel="noopener noreferrer"&gt;https://icecast.org/&lt;/a&gt;&lt;br&gt;
Liquid Soap - &lt;a href="https://www.liquidsoap.info/" rel="noopener noreferrer"&gt;https://www.liquidsoap.info/&lt;/a&gt;&lt;br&gt;
ffmpeg - &lt;a href="https://www.ffmpeg.org/" rel="noopener noreferrer"&gt;https://www.ffmpeg.org/&lt;/a&gt;&lt;br&gt;
youtube-dl - &lt;a href="https://youtube-dl.org/" rel="noopener noreferrer"&gt;https://youtube-dl.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These four components will allow us to implement an audio delivery system, over TCP, that randomizes tracks that we pull off of YouTube. I'm going to make episodes of the Rent / Buy / Build podcast available.&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5s6ay6j8bdo6ug3pdty1.jpg" 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%2F5s6ay6j8bdo6ug3pdty1.jpg" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
As we build out this solution, we're going to climb the abstraction tower. We'll start by building and running our containers by hand, using nothing but the docker command. This is the proof-of-concept phase: does the software work together, and can we make all the components play nice and do what we want?&lt;/p&gt;

&lt;p&gt;This first stage is usually a very messy one, with lots of dead-ends, failed experiments and retries. I consider this a feature of the process, not a bug. Containers afford us the freedom to play and experiment, without risk of polluting our system with unwanted packages, unused processes, or broken configurations. If something doesn't work, all we have to do is shut down the container and delete the image.&lt;/p&gt;

&lt;p&gt;After we've worked out precisely which components we need, and how they work together, we'll move onto the orchestration stage. Using Docker Compose, we'll make some infrastructure code that can be used to deploy all of the pieces and parts together, as a cohesive and contained unit. It is at this stage that we capture the relationships between the containerized components and their data stores, explicitly.&lt;/p&gt;

&lt;p&gt;At this point, I like to take a step back and determine if the whole system is complete: does it lack features, user interfaces, or anything else? Anything that is missing usually doesn't exist on its own, and needs to be built. For this particular project, we're going to need a pretty web interface for interacting with our radio station, and driving new track ingestion.&lt;/p&gt;

&lt;p&gt;I specifically wait until I've finished assembling components before I go off and build any of the missing bits. I find that this helps to (a) keep the scope of anything I build to a minimum, and (b) ensures that the main backbone of the system is solid and functional before I expend any substantial effort writing glue code. Custom software ain't cheap; not only does developing software take time from other projects, but it also places some non-trivial obligation on future-you's time, vis-a-vis maintenance work.&lt;/p&gt;

&lt;p&gt;Once all the gaps are filled, and we're happy with the solution, we move onto the final stage: converting our docker compose recipe into a Kubernetes deployment or three, so that we can run and scale and survive node outages. While this stage builds heavily on the previous stages – Docker Compose and Kubernetes share a lot of the same concepts, after all – it also demands its own specialized bag of tricks.&lt;/p&gt;

&lt;p&gt;When all is said and done, we'll have taken our idea – wouldn't it be cool to run an Internet radio station – from concept to production-grade implementation, using containers all the way.&lt;/p&gt;

&lt;p&gt;Let's get started, shall we?&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jg2r9lcb83vos7qg9xk.jpg" 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%2F9jg2r9lcb83vos7qg9xk.jpg" alt="Alt Text"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://starkandwayne.com/blog/implementing-non-trivial-containerized-systems-part-1-picking-components/" rel="noopener noreferrer"&gt;Join us next week for part 1&lt;/a&gt;, where we dive into assembling the off-the-shelf components into a working system, fit for containerization.&lt;/p&gt;

</description>
      <category>containers</category>
      <category>tutorial</category>
      <category>cloud</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
