<?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: Peter Cooper</title>
    <description>The latest articles on DEV Community by Peter Cooper (@peterc).</description>
    <link>https://dev.to/peterc</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%2F136748%2F91dfd3e4-4e22-4153-ac10-81bf7a4e8557.png</url>
      <title>DEV Community: Peter Cooper</title>
      <link>https://dev.to/peterc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/peterc"/>
    <language>en</language>
    <item>
      <title>How to Quickly Play with PostgreSQL 15 Beta using Docker</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Wed, 25 May 2022 10:32:57 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-quickly-play-with-postgresql-15-beta-using-docker-17hh</link>
      <guid>https://dev.to/peterc/how-to-quickly-play-with-postgresql-15-beta-using-docker-17hh</guid>
      <description>&lt;p&gt;The &lt;a href="https://www.postgresql.org/about/news/postgresql-15-beta-1-released-2453/" rel="noopener noreferrer"&gt;first beta of Postgres 15&lt;/a&gt; is out now and the official Docker image for it has also just gone live, making it easy for you to take it for a quick spin without destroying your production systems.&lt;/p&gt;

&lt;p&gt;Assuming you have Docker running, here's the quickest way to get going:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker network create pgnetwork
docker run --name pg15beta1 --network pgnetwork -e POSTGRES_PASSWORD=whatever -d postgres:15beta1 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a network called &lt;code&gt;pgnetwork&lt;/code&gt; so that other containers can communicate with Postgres easily. A container is then run based on the &lt;code&gt;postgres:15beta1&lt;/code&gt; image, a default password is set for the &lt;code&gt;postgres&lt;/code&gt; user (you can change this to whatever you like), and the container is given the name of &lt;code&gt;pg15beta1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then to run &lt;code&gt;psql&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;docker run -it --rm --network pgnetwork postgres:15beta1 psql -h pg15beta1 -U postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spins up &lt;em&gt;another&lt;/em&gt; container (which is deleted after you quit &lt;code&gt;psql&lt;/code&gt; thanks to the &lt;code&gt;--rm&lt;/code&gt; option) which uses the same Postgres image but to run &lt;code&gt;psql&lt;/code&gt; instead. Enter the password you provided in the first step and you're in.&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%2Fg4u0xvc9l2o6y9ffehhj.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%2Fg4u0xvc9l2o6y9ffehhj.png" alt="Everything is working with psql"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To clean up:&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 pg15beta1
docker rm pg15beta1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have &lt;code&gt;psql&lt;/code&gt; locally and want to experiment with Postgres 15 with apps on your local system, you could expose its port on localhost 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;docker run --name pg15beta1 -e POSTGRES_PASSWORD=whatever -p 1234:5432 -d postgres:15beta1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This then makes the Postgres 15 server accessible at port &lt;code&gt;1234&lt;/code&gt; on localhost. Tweak to suit your use case.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>postgres</category>
    </item>
    <item>
      <title>How to Create a Bootable USB Stick from the Windows 11 ISO on macOS</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Mon, 23 May 2022 10:30:39 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-create-a-bootable-usb-stick-from-the-windows-11-iso-on-macos-3708</link>
      <guid>https://dev.to/peterc/how-to-create-a-bootable-usb-stick-from-the-windows-11-iso-on-macos-3708</guid>
      <description>&lt;p&gt;An unused &lt;a href="https://www.intel.com/content/www/us/en/products/details/nuc.html"&gt;Intel NUC&lt;/a&gt; has been staring at me from across my living room for months, so I decided to torture it with a Windows 11 install.&lt;/p&gt;

&lt;p&gt;Microsoft has &lt;a href="https://www.microsoft.com/software-download/windows11"&gt;a Windows 11 ISO&lt;/a&gt; you can download now, so I figured I'd use BalenaEtcher to 'burn' it on to a USB stick and boot the NUC from it. Easy. Wrong.&lt;/p&gt;

&lt;p&gt;Creating a USB stick from the Windows 11 ISO that &lt;em&gt;actually boots&lt;/em&gt; is tricky on macOS. &lt;a href="https://www.balena.io/etcher/"&gt;BalenaEtcher&lt;/a&gt;, an otherwise useful and common tool for copying images to USB sticks, doesn't like Microsoft's offering for annoying, complicated reasons.&lt;/p&gt;

&lt;p&gt;Luckily, there is a way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make sure &lt;a href="https://brew.sh/"&gt;Homebrew&lt;/a&gt; is installed. If you're a dev, it probably is.&lt;/li&gt;
&lt;li&gt;Mount the Windows 11 ISO in macOS so it appears on the desktop and is available at &lt;code&gt;/Volumes/CCCOMA_X64FRE_EN-GB_DV9&lt;/code&gt; or similar.&lt;/li&gt;
&lt;li&gt;Put the USB stick in.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;diskutil list&lt;/code&gt; at the terminal to find your USB stick. It'll be something like &lt;code&gt;/dev/disk5&lt;/code&gt; – I'll pretend it is for the rest of these instructions.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;diskutil eraseDisk MS-DOS "w11" MBR disk5&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rsync -avh -progress -exclude=sources/install.wim /Volumes/CCCOMA_X64FRE_EN-GB_DV9/ /Volumes/w11&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;brew install wimlib&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wimlib-imagex split /Volumes/CCCOMA_X64FRE_EN-GB_DV9/sources/install.wim /Volumes/w11/sources/install.swm 4000&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ta-da.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Please note that certain names and numbers above will vary depending on locale or configuration. Make sure to update those as you follow the instructions.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>windows</category>
      <category>macos</category>
    </item>
    <item>
      <title>How to Install the `nano` Text Editor on AWS CloudShell</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Tue, 15 Dec 2020 21:58:05 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-install-the-nano-text-editor-on-aws-cloudshell-23cc</link>
      <guid>https://dev.to/peterc/how-to-install-the-nano-text-editor-on-aws-cloudshell-23cc</guid>
      <description>&lt;p&gt;I know the basics of &lt;code&gt;vim&lt;/code&gt; and I &lt;em&gt;even&lt;/em&gt; know how to exit it(!) but I'd much rather use &lt;a href="https://www.nano-editor.org/"&gt;GNU Nano&lt;/a&gt; as my editor at the terminal. Yet, the Amazon Linux 2 distribution with AWS CloudShell doesn't include it!&lt;/p&gt;

&lt;p&gt;Here's what to do to install it into your home folder so that it remains in place long-term:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Install basic developer tools and dependencies
sudo yum -y groupinstall "Development Tools"
sudo yum -y install git-core zlib zlib-devel gcc-c++ patch readline readline-devel libffi-devel openssl-devel make bzip2 autoconf automake libtool bison curl sqlite-devel xz

# Download and compile nano
cd ~
mkdir ~/bin
wget https://www.nano-editor.org/dist/v5/nano-5.4.tar.xz
tar xf nano-5.4.tar.xz
cd nano-5.4
./configure --prefix=$HOME
make &amp;amp;&amp;amp; make install

# Clean up and add ~/bin to PATH
cd ..
rm -rf nano-5.4
rm nano-5.4.tar.xz
echo 'export PATH="$HOME/bin:$PATH"' &amp;gt;&amp;gt; ~/.bash_profile
source ~/.bash_profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can run &lt;code&gt;nano&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;nano
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>nano</category>
      <category>linux</category>
      <category>aws</category>
    </item>
    <item>
      <title>How to Install Ruby on AWS CloudShell</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Tue, 15 Dec 2020 21:32:58 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-install-ruby-on-aws-cloudshell-3n8c</link>
      <guid>https://dev.to/peterc/how-to-install-ruby-on-aws-cloudshell-3n8c</guid>
      <description>&lt;p&gt;If you use AWS and like to interact with AWS services from the shell/terminal, there's a new option from today: &lt;a href="https://aws.amazon.com/blogs/aws/aws-cloudshell-command-line-access-to-aws-resources/"&gt;AWS CloudShell.&lt;/a&gt; CloudShell provides a terminal emulator &lt;em&gt;in the browser&lt;/em&gt; complete with the tools and authentication credentials needed to work with your AWS resources.&lt;/p&gt;

&lt;p&gt;Python, Node, and numerous other tools are preinstalled on the Amazon Linux 2 based instance out of the box, but.. there's no Ruby! Boo!&lt;/p&gt;

&lt;p&gt;While installing Ruby on Amazon Linux 2 is no big deal, only the &lt;code&gt;/home&lt;/code&gt; folder is persisted between AWS CloudShell runs, so you'll need to take extra care to install it under your home directory.&lt;/p&gt;

&lt;p&gt;Without further ado, here's how to get Ruby 2.7.2 running on AWS CloudShell using &lt;code&gt;rbenv&lt;/code&gt; and &lt;code&gt;ruby-build&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;# Install basic developer tools and dependencies
sudo yum -y groupinstall "Development Tools"
sudo yum -y install git-core zlib zlib-devel gcc-c++ patch readline readline-devel libffi-devel openssl-devel make bzip2 autoconf automake libtool bison curl sqlite-devel

# Install libyaml to $HOME
wget https://github.com/yaml/libyaml/releases/download/0.2.5/yaml-0.2.5.tar.gz
tar xzf yaml-0.2.5.tar.gz 
cd yaml-0.2.5
./configure --prefix=$HOME
make &amp;amp;&amp;amp; make install
cd ..
rm -rf yaml-0.2.5
rm yaml-0.2.5.tar.gz

# Install rbenv and ruby-build
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv &amp;amp;&amp;amp; src/configure &amp;amp;&amp;amp; make -C src
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' &amp;gt;&amp;gt; ~/.bash_profile
~/.rbenv/bin/rbenv init
echo 'eval "$(rbenv init -)"' &amp;gt;&amp;gt; ~/.bash_profile
source ~/.bash_profile
mkdir -p "$(rbenv root)"/plugins
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

# Install Ruby itself
RUBY_CONFIGURE_OPTS=--with-libyaml-dir=$HOME rbenv install 2.7.2
rbenv global 2.7.2
gem env home
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a LOT to digest here, but here's basically what happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install basic development dependencies (&lt;code&gt;gcc&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;libyaml&lt;/code&gt; manually to get it into &lt;code&gt;/home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;rbenv&lt;/code&gt; and &lt;code&gt;ruby-build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Get &lt;code&gt;ruby-build&lt;/code&gt; to build Ruby 2.7.2&lt;/li&gt;
&lt;li&gt;Set Ruby 2.7.2 as our default, global Ruby&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;gem env home&lt;/code&gt; just to see everything is working&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that due to &lt;em&gt;only&lt;/em&gt; the &lt;code&gt;/home&lt;/code&gt; directory sticking around long term, you might find that some gems with native dependencies fail to install. If the dependencies are binary packages, you'll need to install them prior to installing or upgrading those gems.&lt;/p&gt;

&lt;p&gt;To install GCC and a big pile of common native dependencies, you can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum groupinstall "Development Tools"
sudo yum install git-core zlib zlib-devel gcc-c++ patch readline readline-devel libyaml-devel libffi-devel openssl-devel make bzip2 autoconf automake libtool bison curl sqlite-devel libyaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Notes on an earlier &lt;code&gt;libyaml&lt;/code&gt; problem..
&lt;/h2&gt;

&lt;p&gt;In an earlier version of this article, I used the normal &lt;code&gt;libyaml&lt;/code&gt; packages and Ruby installed fine.. but once the instance had been deleted due to inactivity, &lt;code&gt;gem list&lt;/code&gt; failed to work due to &lt;code&gt;libyaml&lt;/code&gt; being dynamically loaded from the system.&lt;/p&gt;

&lt;p&gt;The error looked 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;in 'require': libyaml-0.so.2: cannot open shared object file: No such file or directory - /home/cloudshell-user/.rbenv/versions/2.7.2/lib/ruby/2.7.0/x86_64-linux/psych.so (LoadError)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you really want to use the system &lt;code&gt;libyaml&lt;/code&gt;, this can be resolved by reinstalling &lt;code&gt;libyaml&lt;/code&gt; each time using &lt;code&gt;sudo yum -y install libyaml&lt;/code&gt; and this could be added to &lt;code&gt;~/.bash_profile&lt;/code&gt; but if you install &lt;code&gt;libyaml&lt;/code&gt; to &lt;code&gt;$HOME&lt;/code&gt; as in the new instructions up top, this problem goes away.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ruby</category>
      <category>cloudshell</category>
    </item>
    <item>
      <title>How to Create a AWS Lambda Layer of Your Gemfile / Ruby Gem Dependencies</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Sun, 06 Dec 2020 00:06:19 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-create-a-aws-lambda-layer-of-your-gemfile-ruby-gem-dependencies-1gfj</link>
      <guid>https://dev.to/peterc/how-to-create-a-aws-lambda-layer-of-your-gemfile-ruby-gem-dependencies-1gfj</guid>
      <description>&lt;p&gt;If you use rooling like &lt;a href="https://github.com/aws/aws-sam-cli" rel="noopener noreferrer"&gt;AWS SAM&lt;/a&gt; or &lt;em&gt;Serverless Framework&lt;/em&gt; to build and deploy your serverless Ruby functions, they'll take care of building your dependencies on a function by function basis and package them up for deployment. Great! Use them!&lt;/p&gt;

&lt;p&gt;But if you prefer to have your own hands on the controls at all times, using gems with native dependencies (e.g. Nokogiri) can be a &lt;em&gt;nightmare!&lt;/em&gt; If you prefer to travel light (or, maybe, like me you like coding adhoc functions in the AWS console), you can instead build your dependencies into a &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html" rel="noopener noreferrer"&gt;Lambda Layer&lt;/a&gt; and then use that layer across whatever Lambda functions you like without worrying about dependencies again.&lt;/p&gt;

&lt;p&gt;But how? After an hour of trial and error plus a few tips from &lt;a href="https://medium.com/@joshua.a.kahn/exploring-aws-lambda-layers-and-ruby-support-5510f81b4d14" rel="noopener noreferrer"&gt;this neat article&lt;/a&gt; (though that one still used SAM), &lt;strong&gt;I figured out the magic spell required to locally build Ruby gems and their dependencies in a local Amazon Linux container and turn them into a layer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: This post assumes you're familiar with the &lt;code&gt;aws&lt;/code&gt; client and have the credentials to use Lambda with it. If not, this is too advanced for now. Google the &lt;code&gt;aws&lt;/code&gt; CLI client and how to create an IAM user with the right permissions.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;Without further ado, here's the magic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LAYER_NAME="my-ruby-layer"
mkdir $LAYER_NAME &amp;amp;&amp;amp; cd $_

bundle init
bundle add http --skip-install
bundle add nokogiri --skip-install
rm Gemfile.lock

docker run --rm -v $PWD:/var/layer \
           -w /var/layer \
           amazon/aws-sam-cli-build-image-ruby2.7 \
           bundle install --path=ruby

mv ruby/ruby ruby/gems
zip -r layer.zip ruby

aws lambda publish-layer-version \
           --layer-name $LAYER_NAME \
           --region eu-west-1 \
           --compatible-runtimes ruby2.7 \
           --zip-file fileb://layer.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This results in a &lt;code&gt;LayerVersionArn&lt;/code&gt; you can use with your Lambda functions. Once you've done this, loading the gems you need in the usual way (e.g. &lt;code&gt;require 'nokogiri'&lt;/code&gt;) will Just Work™.&lt;/p&gt;

&lt;p&gt;If you use the AWS console, it'll let you pick this from a drop down menu which is how I like to do it:&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%2Fi%2Fhswsfvgifcexn17f7sbc.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%2Fhswsfvgifcexn17f7sbc.png" alt="Picking a Lambda layer in the AWS console"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the code works
&lt;/h2&gt;

&lt;p&gt;Here's a quick break down of what the code above &lt;em&gt;does.&lt;/em&gt; First, we create a folder with the name of our layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LAYER_NAME="my-ruby-layer"
mkdir $LAYER_NAME &amp;amp;&amp;amp; cd $_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we create a &lt;code&gt;Gemfile&lt;/code&gt; and add some gems to it. If you already have a &lt;code&gt;Gemfile&lt;/code&gt;, copy it in at this point instead of doing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle init
bundle add http --skip-install
bundle add nokogiri --skip-install
rm Gemfile.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: I delete Gemfile.lock because the version of Bundler in the Lambda container clashes with mine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's now instruct Docker to run the image Amazon provides for building Ruby 2.7 dependencies and do the &lt;code&gt;bundle install&lt;/code&gt; there (while saving it to our normal filesystem):&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 --rm -v $PWD:/var/layer \
           -w /var/layer \
           amazon/aws-sam-cli-build-image-ruby2.7 \
           bundle install --path=ruby
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick directory name tweak is necessary to match the structure that Lambda expects. We can then zip up the &lt;code&gt;ruby&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mv ruby/ruby ruby/gems
zip -r layer.zip ruby
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, publish the layer to AWS Lambda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws lambda publish-layer-version \
           --layer-name $LAYER_NAME \
           --region eu-west-1 \
           --compatible-runtimes ruby2.7 \
           --zip-file fileb://layer.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to set the region to the one you actually want to use the layer from. Every region has its own layers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: If you want to deploy your layer to the public or use it more broadly across all regions, you'll want to search up the AWS Serverless Application Repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rubygems</category>
      <category>aws</category>
      <category>serverless</category>
    </item>
    <item>
      <title>How to Look at Your GitHub API Rate Limits</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Sat, 05 Dec 2020 18:14:23 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-look-at-your-github-api-rate-limits-29m1</link>
      <guid>https://dev.to/peterc/how-to-look-at-your-github-api-rate-limits-29m1</guid>
      <description>&lt;p&gt;If you're using any GitHub client library worth its salt, it'll have a way to look at your current rate limits and expiry times. I have lots of tools using my GitHub account, however, and wanted an independent way to look at this data.&lt;/p&gt;

&lt;p&gt;Luckily, if you have a &lt;a href="https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token"&gt;Personal Access Token (PAT)&lt;/a&gt; it's easy to do so.&lt;/p&gt;

&lt;p&gt;Place your personal access token in &lt;code&gt;~/.githubtoken&lt;/code&gt; (or any file of your choice) and then you can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl https://api.github.com/rate_limit -H "Authorization: token $(cat ~/.githubtoken)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, I wanted to be able to use the information in a Ruby script and to show the number of seconds &lt;em&gt;until&lt;/em&gt; a quota were to reset, so this Ruby code fits the bill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require 'http'
require 'json'

TOKEN = File.read(ENV['HOME'] + "/.githubtoken").strip

res = HTTP["Authorization" =&amp;gt; "token #{TOKEN}"].get("https://api.github.com/rate_limit")
rates = JSON.parse(res.to_s)

def convert_epoch_to_seconds_remaining(r)
  r["resets_in_seconds"] = (Time.at(r["reset"].to_i) - Time.now).ceil if r["reset"]
  r.values.each { |rr| convert_epoch_to_seconds_remaining(rr) if rr.is_a?(Hash) }
end

convert_epoch_to_seconds_remaining(rates)

puts rates.to_json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only dependency is the &lt;code&gt;http&lt;/code&gt; gem, but you should have this anyway as it's fantastic. If you'd rather have no dependencies, you could use &lt;code&gt;open-uri&lt;/code&gt; 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;require 'json'
require 'open-uri'

TOKEN = File.read(ENV['HOME'] + "/.githubtoken").strip

res = open("https://api.github.com/rate_limit", "Authorization" =&amp;gt; "token #{TOKEN}").read
rates = JSON.parse(res.to_s)

def convert_epoch_to_seconds_remaining(r)
  r["resets_in_seconds"] = (Time.at(r["reset"].to_i) - Time.now).ceil if r["reset"]
  r.values.each { |rr| convert_epoch_to_seconds_remaining(rr) if rr.is_a?(Hash) }
end

convert_epoch_to_seconds_remaining(rates)

puts rates.to_json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>github</category>
      <category>ruby</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Make curl Request a Site from a Different IP Than in DNS</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Wed, 11 Nov 2020 19:58:36 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-make-curl-request-a-site-from-a-different-ip-than-in-dns-4n45</link>
      <guid>https://dev.to/peterc/how-to-make-curl-request-a-site-from-a-different-ip-than-in-dns-4n45</guid>
      <description>&lt;p&gt;One issue I've encountered over the years has been when moving a Web site between systems and the site is set up on two different servers simultaneously (this could also happen if using a CDN). Let's say &lt;code&gt;example.com&lt;/code&gt; is set up on &lt;code&gt;1.1.1.1&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;2.2.2.2&lt;/code&gt; but DNS is pointing solely to &lt;code&gt;1.1.1.1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What do we do if we want to request &lt;code&gt;http://example.com/&lt;/code&gt; but from  &lt;code&gt;2.2.2.2&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;A system wide approach would be adding an entry to the &lt;code&gt;/etc/hosts&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2.2.2.2 example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This usually works but can require you to flush your local DNS cache and/or force refresh in your browser. You have to remember to remove the line as well, it's not very configurable, and so on.&lt;/p&gt;

&lt;p&gt;What about if we want to use &lt;code&gt;curl&lt;/code&gt;? You &lt;em&gt;could&lt;/em&gt; use &lt;code&gt;/etc/hosts&lt;/code&gt; but for debugging purposes it'd be nice if we could override the IP address on a single request basis as part of the command. This is possible!&lt;/p&gt;

&lt;p&gt;The way I &lt;em&gt;used&lt;/em&gt; to do it was to override the &lt;code&gt;Host&lt;/code&gt; HTTP header (the way a HTTP client tells a server which host it's looking for) and request from the new IP address, 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;curl -H "Host: example.com" http://2.2.2.2/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine in the old days, but in the modern HTTPS world various problems are introduced. Luckily &lt;code&gt;curl&lt;/code&gt; has a solution in the form of &lt;code&gt;--resolve&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;--resolve &amp;lt;host:port:address[,address]...&amp;gt;
       Provide a custom address for a  specific  host  and  port  pair.
       Using  this,  you  can make the curl requests(s) use a specified
       address and prevent the otherwise normally resolved  address  to
       be  used.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if we want to request &lt;code&gt;example.com&lt;/code&gt; from &lt;code&gt;2.2.2.2&lt;/code&gt;, we could do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl http://example.com/ --resolve example.com:80:2.2.2.2 
curl https://example.com/ --resolve example.com:443:2.2.2.2 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiple entries can be added if needed for more complex situations like chains of redirects, say.&lt;/p&gt;

&lt;p&gt;It's also possible to use &lt;code&gt;*&lt;/code&gt; as a wildcard host so that any host involved in the request goes to the address you specify (and it could also save you a bit of redundancy and typing!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl http://example.com/ --resolve *:80:2.2.2.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>dns</category>
      <category>curl</category>
    </item>
    <item>
      <title>Print Files But Without the First Line at the Terminal</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Sun, 08 Nov 2020 22:53:10 +0000</pubDate>
      <link>https://dev.to/peterc/print-files-but-without-the-first-line-at-the-terminal-2he4</link>
      <guid>https://dev.to/peterc/print-files-but-without-the-first-line-at-the-terminal-2he4</guid>
      <description>&lt;p&gt;I had a bunch of CSV files I wanted to join together and they all had the same 'header' line at the start. I didn't want the header line repeating in my concatenation of the files so I wanted to get all &lt;em&gt;except&lt;/em&gt; the first line for my concatenation process (using &lt;code&gt;cp&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Turns out there are several ways to do this. Let's say you have a file &lt;code&gt;a.txt&lt;/code&gt; containing such:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
2
3
4
5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your goal is to get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2
3
4
5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can't use &lt;code&gt;tail&lt;/code&gt; in the standard way as you might not know how long the file is. But you can use it with a special &lt;code&gt;+&lt;/code&gt; number to specify that &lt;code&gt;tail&lt;/code&gt; is to &lt;em&gt;begin&lt;/em&gt; on a certain line, 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;tail -n +2 a.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use &lt;code&gt;awk&lt;/code&gt; to get the job done by specifying that it can return any lines where the line number count is larger than 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;awk 'NR&amp;gt;1' a.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you could use &lt;code&gt;sed&lt;/code&gt; to delete the first line before displaying the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sed '1d' a.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each approach has various pros and cons mostly around which one you can remember in the moment or if you want to use &lt;code&gt;awk&lt;/code&gt; or &lt;code&gt;sed&lt;/code&gt; in other ways to make other adjustments, but it's handy to have options here.&lt;/p&gt;

&lt;p&gt;To conclude, I took my CSV files and joined them 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;cp 1.csv all.csv
tail -n +2 2.csv &amp;gt;&amp;gt; all.csv
tail -n +2 3.csv &amp;gt;&amp;gt; all.csv
tail -n +2 4.csv &amp;gt;&amp;gt; all.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are smarter ways to do this all in one go, but I only had a few files to do anyway! :-)&lt;/p&gt;

</description>
      <category>linux</category>
      <category>terminal</category>
      <category>awk</category>
      <category>tail</category>
    </item>
    <item>
      <title>How to install the Kristall Gemini client on Raspberry Pi OS</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Fri, 06 Nov 2020 21:08:37 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-install-the-kristall-gemini-client-on-raspberry-pi-os-ag</link>
      <guid>https://dev.to/peterc/how-to-install-the-kristall-gemini-client-on-raspberry-pi-os-ag</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/MasterQ32/kristall"&gt;Kristall&lt;/a&gt; is an open source client for &lt;a href="https://gemini.circumlunar.space/"&gt;Gemini&lt;/a&gt;, a protocol and set of technologies for lo-fi, high performance hypertext.&lt;/p&gt;

&lt;p&gt;There are no binary builds for the ARM architecture of the Pi and you need to install some Qt5 related packages. This post documents how I got things running on standard Raspberry Pi OS on a Raspberry Pi 400.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/MasterQ32/kristall.git
cd kristall
sudo apt-get install qt5-default qtmultimedia5-dev libqt5svg5-dev
make
./kristall
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beyond this point you could copy &lt;code&gt;kristall&lt;/code&gt; to &lt;code&gt;/usr/local/bin&lt;/code&gt; or wherever you want to run it from, as well as install the desktop file to get things into the application menu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mv kristall /usr/local/bin/
cp Kristall.desktop ~/.local/share/applications/kristall.desktop
update-desktop-database ~/.local/share/applications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>raspberrypi</category>
      <category>kristall</category>
      <category>gemini</category>
    </item>
    <item>
      <title>List unique paths requested by popularity in NGINX logs (access.log)</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Mon, 19 Oct 2020 12:57:47 +0000</pubDate>
      <link>https://dev.to/peterc/list-unique-paths-requested-by-popularity-in-nginx-logs-access-log-53j2</link>
      <guid>https://dev.to/peterc/list-unique-paths-requested-by-popularity-in-nginx-logs-access-log-53j2</guid>
      <description>&lt;p&gt;We had a big pile of NGINX &lt;code&gt;access.log&lt;/code&gt; files for our site and wanted to quickly know all of the unique paths that had been requested.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;access.log&lt;/code&gt; file(s) follow a reasonably standard format that looks 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;127.0.154.222 - - [19/Oct/2020:06:26:59 +0000] "GET / HTTP/1.1" 301 178 "-" "-"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;.. then you can use this solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;awk -F\" '{print $2}' access.log | awk '{print $2}' | sort | uniq -c | sort -g
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output will look 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;[lots of stuff here]
    104 /xmlrpc.php
    114 /wp-includes/wlwmanifest.xml
    121 /robots.txt
    161 /feed/
    336 /
   3056 //xmlrpc.php
  53786 /wp-login.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what's going on?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;awk -F\" '{print $2}' access.log&lt;/code&gt; splits each line on the first quotation mark and returns the second part.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;awk '{print $2}'&lt;/code&gt; then skips the HTTP verb (GET/POST/PUT/etc.) and prints out the path (which follows the space after the HTTP verb).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sort&lt;/code&gt; sorts the output into groups of the same thing which..&lt;/p&gt;

&lt;p&gt;&lt;code&gt;uniq -c&lt;/code&gt; then turns into a list of the unique paths only. The &lt;code&gt;-c&lt;/code&gt; prefixes the output with the number of non-unique lines.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sort -g&lt;/code&gt; then sorts the lines in numeric order.&lt;/p&gt;

&lt;p&gt;Want the result in descending numeric order? Use &lt;code&gt;sort -gr&lt;/code&gt; instead.&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>logs</category>
      <category>awk</category>
    </item>
    <item>
      <title>How to manually import files into Middleman site builds</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Tue, 13 Oct 2020 16:14:33 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-manually-import-files-into-middleman-site-builds-410p</link>
      <guid>https://dev.to/peterc/how-to-manually-import-files-into-middleman-site-builds-410p</guid>
      <description>&lt;p&gt;&lt;a href="https://middlemanapp.com/"&gt;Middleman&lt;/a&gt; is a neat Ruby-based static site generator that provides a good alternative to Jekyll if you want something simpler and not focused on blogging. As such, we're using it for our new company site.&lt;/p&gt;

&lt;p&gt;We ran into an annoying bug with it, however.&lt;/p&gt;

&lt;p&gt;We're using &lt;a href="https://www.netlify.com/"&gt;Netlify&lt;/a&gt; to host the new, Middleman-built site and Netlify lets you use a &lt;a href="https://docs.netlify.com/routing/redirects/"&gt;&lt;code&gt;_redirects&lt;/code&gt; file&lt;/a&gt; to specify all of the redirected URLs you want on your site. Great, except that if you place a &lt;code&gt;_redirects&lt;/code&gt; file in your Middleman site, it doesn't get copied across into the final build of the site!&lt;/p&gt;

&lt;p&gt;It turns out the reason for this is that Middleman treats files beginning with an underscore as special (since they could be &lt;a href="https://middlemanapp.com/basics/partials/"&gt;partials&lt;/a&gt;) and you need to manually specify the inclusion of such static files in the build.&lt;/p&gt;

&lt;p&gt;Add this to &lt;code&gt;config.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import_file File.expand_path("_redirects", config[:source]), "/_redirects"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;import_file&lt;/code&gt; manually brings a file from one location to another in your final build, so this brings the &lt;code&gt;_redirects&lt;/code&gt; file in the site's 'source' directory to live at &lt;code&gt;/_redirects&lt;/code&gt; in the final site and Netlify is happy!&lt;/p&gt;

</description>
      <category>middleman</category>
      <category>ruby</category>
      <category>static</category>
      <category>ssg</category>
    </item>
    <item>
      <title>How to Create Joined Bulletpoint Lists with CSS, BBC News-style</title>
      <dc:creator>Peter Cooper</dc:creator>
      <pubDate>Tue, 07 May 2019 11:56:30 +0000</pubDate>
      <link>https://dev.to/peterc/how-to-create-joined-bulletpoint-lists-with-css-bbc-news-style-1eem</link>
      <guid>https://dev.to/peterc/how-to-create-joined-bulletpoint-lists-with-css-bbc-news-style-1eem</guid>
      <description>&lt;p&gt;When there are live events, &lt;a href="https://www.bbc.co.uk/news" rel="noopener noreferrer"&gt;BBC News&lt;/a&gt; often runs a 'timeline' style list against stories. For example:&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%2Fqcdls8psrzd9wtlpx86y.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%2Fqcdls8psrzd9wtlpx86y.png" alt="The effect on the BBC News site"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was intrigued to work out how they'd got the bullet points of the list items to be dynamically joined by a line, and set off with my trusty Chrome DevTools to figure it out.&lt;/p&gt;

&lt;p&gt;And, boy, their markup is a trip!&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%2Fp1cq0absnmromh0gqd95.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%2Fp1cq0absnmromh0gqd95.jpg" alt="It's an adventure.."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Long story short, it's a standard HTML list (the BBC uses &lt;code&gt;&amp;lt;ol&amp;gt;&lt;/code&gt; but I went with &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;) where each list item (&lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;) has a &lt;code&gt;:before&lt;/code&gt; pseudo-element that's empty content-wise but is marked as being 2 pixels wide with a red background color. This creates the 'line' before each &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;. Further styling then positions this pseudo-element/line.&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%2Frik69vexvchzgs9l28d1.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%2Frik69vexvchzgs9l28d1.png" alt="a debug style view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key part of the CSS is for the pseudo-element:&lt;/p&gt;

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

&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="nd"&gt;:before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#c00&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&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;You also need to make sure that the list item itself uses &lt;code&gt;relative&lt;/code&gt; positioning so that the &lt;code&gt;position: absolute&lt;/code&gt; on the pseudo-element doesn't base itself on the page but on the list item itself:&lt;/p&gt;

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

&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* You need to turn on relative positioning so the line is placed relative to the item rather than absolutely on the page */&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* Use padding to space things out rather than margins as the line would get broken up otherwise */&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20px&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;&lt;em&gt;(Also note that if you want to have gaps between your line items now, you need to use padding, otherwise the line would get broken up by the margins!)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The bullet-points aren't the standard HTML bullet-points but are just circles drawn using inline SVG. You could in theory use any shape (stars, squares, whatever) or maybe even something like an emoji, but I just copied the circle approach that the BBC uses.&lt;/p&gt;

&lt;p&gt;Rather than bore you to tears with a technical explanation of how it works, I've boiled it down to the simplest code example I could come up with, added plentiful comments, and put it on CodePen for you to fiddle around with.&lt;/p&gt;

&lt;p&gt;Play with it here:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/peterc/embed/vwEJJN?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;LATE ADDITION:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/saadatm" rel="noopener noreferrer"&gt;Saadat&lt;/a&gt; on Twitter has extended the concept above a little more by baking the SVG for the bullets into the CSS by using the &lt;code&gt;:after&lt;/code&gt; pseudo-element! This makes the code even cleaner. Their code is &lt;a href="https://codepen.io/anon/pen/gJpLrR" rel="noopener noreferrer"&gt;in this pen&lt;/a&gt; but the crux of it is this:&lt;/p&gt;

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

&lt;span class="c"&gt;/* Small bullets for normal list items */&lt;/span&gt;
&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' viewBox='0 0 32 32' focusable='false'%3E%3Ccircle stroke='none' fill='%23c00' cx='16' cy='16' r='10'%3E%3C/circle%3E%3C/svg%3E")&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-repeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;no-repeat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&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;

</description>
      <category>html</category>
      <category>css</category>
      <category>li</category>
      <category>list</category>
    </item>
  </channel>
</rss>
