<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>chrisblunt.com</title>
    <description>Chris is a Software and Data Engineer based in Plymouth, UK.

• Founder of Plymouth Software.
• Works with Ruby on Rails, Docker, AWS, SQL/Pg
• Tea &amp; coffee drinker.

Follow on bluesky • github • linkedin • rss • email</description>
    <link>https://www.chrisblunt.com/</link>
    <item>
      <title>2026-05-17 Moment</title>
      <description>
        <![CDATA[<p><br></p><figure class="attachment attachment--preview attachment--jpg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/7qcp29bom7sf76tmhk024o3muo6z" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/7qcp29bom7sf76tmhk024o3muo6z">

    <figcaption class="attachment__caption">
      Footpath leading to beach and Woolacombe, North Devon.
    </figcaption>
</figure><p>Walk to Woolacombe and Morthoe, North Devon.</p>
]]>
      </description>
      <pubDate>Sun, 17 May 2026 22:32:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2026-05-17-moment</link>
      <guid>https://www.chrisblunt.com/2026-05-17-moment</guid>
      <category>devon</category>
      <category>moment</category>
      <category>nature</category>
      <category>walks</category>
    </item>
    <item>
      <title>2026-05-04 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/sfot612myfjsa5ubanq9v8us22ni" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/sfot612myfjsa5ubanq9v8us22ni">

</figure><p>Looe, #Cornwall</p>
]]>
      </description>
      <pubDate>Mon, 04 May 2026 16:09:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2026-05-04-moment</link>
      <guid>https://www.chrisblunt.com/2026-05-04-moment</guid>
      <category>beach</category>
      <category>cornwall</category>
      <category>moment</category>
    </item>
    <item>
      <title>2026-04-30 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/23yv0pr9nlkqdtdncx5eii0bmeqs" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/23yv0pr9nlkqdtdncx5eii0bmeqs">

</figure><p>Exeter Cathedral, Devon.</p>
]]>
      </description>
      <pubDate>Thu, 30 Apr 2026 13:22:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2026-04-30-moment</link>
      <guid>https://www.chrisblunt.com/2026-04-30-moment</guid>
      <category>devon</category>
      <category>exeter</category>
      <category>moment</category>
    </item>
    <item>
      <title>Writing from Obsidian</title>
      <description>
        <![CDATA[<p>One of the most useful features of <a href="https://www.pagecord.com">Pagecord</a> is its <a href="https://help.pagecord.com/api">API</a>.  </p>

<p>As I mentioned in my <a href="https://www.chrisblunt.com/moving-to-pagecord">previous post</a>, this has allowed me to migrate existing content from my Markdown files using a custom CLI tool I've written in Ruby. It also means that - thanks to the <a href="https://github.com/lylo/obsidian-pagecord">obsidian-pagecord</a> plugin - I can use Obsidian to write and publish content.</p>

<p>Email posting is the headline feature that has drawn me to Pagecord for friction-less publishing, but whilst HTML formatting is supported, it doesn't parse Markdown in the emails. </p>

<p>For those longer posts where I would like to be more precise about headings, links, code blocks, etc., the Obsidian Pagecord plugin means I can write in a familiar - and powerful - Markdown editor, whilst easily publishing directly to the site.</p>
]]>
      </description>
      <pubDate>Thu, 30 Apr 2026 12:32:04 +0100</pubDate>
      <link>https://www.chrisblunt.com/writing-from-obsidian</link>
      <guid>https://www.chrisblunt.com/writing-from-obsidian</guid>
    </item>
    <item>
      <title>2026-04-29 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/6r6qswagfunbcyijc2ndfhsxgf3u" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/6r6qswagfunbcyijc2ndfhsxgf3u">

</figure><p>☕ Morning #coffeeandcode at Elements Cafe, Plymouth Science Park.</p>
]]>
      </description>
      <pubDate>Wed, 29 Apr 2026 09:04:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2026-04-29-moment</link>
      <guid>https://www.chrisblunt.com/2026-04-29-moment</guid>
      <category>coffee</category>
      <category>moment</category>
      <category>plymouth</category>
    </item>
    <item>
      <title>Moving to Pagecord</title>
      <description>
        <![CDATA[<p>A long time ago, I used an app called <a href="https://www.chrisblunt.com/moving-to-posterous">Posterous</a> for this blog. Posterous' main feature was the ability to post directly via email, skipping the inconvenience of logins and custom editors and reducing posting friction.</p><p>Ever since it was shut down, I've wished to replicate that simplicity in my publishing. I've tried <a href="https://www.chrisblunt.com/moving-to-blot">various</a> <a href="https://www.chrisblunt.com/switching-to-jekyll">blogging</a> <a href="https://www.chrisblunt.com/indecision">platforms</a>; started writing my own apps; and put together solutions via tools like Zapier - but nothing felt quite as elegant and efficient as Posterous.</p><p>Most recently I've been using <a href="https://www.11ty.dev/">11ty</a> to power this site, but - even though I spend most of my working time in a code editor - I've never found using Zed or VSCode or Vim, or any code editor, a good place for writing.</p><p>Coupled with the fact I would procrastinate by running npm updates, wrangling with JavaScript, and trying out the latest version of [insert any framework/tool/package here] to get my site "just right", it has meant that I've always spent more time updating the code rather than the content.</p><p>I've always found myself settling back to a static site generator, and the hope that I'll "just write more". That's often not the case though.</p><p>So I've been been interested to see the  the development of <a href="https://pagecord.com/">Pagecord</a>, and light but increasingly-powerful blogging platform written by <a href="https://olly.world/">Olly Headey</a>.</p><p>(As an aside, Olly was one of the co-founders of FreeAgent - the bookkeeping software I've used since day one of <a href="https://www.plymouthsoftware.com/">my business</a>, and wouldn't be without 🙏)</p><h2 id="back-to-email">Back to Email</h2><p>Like Posterous, Pagecord is based on the same idea of posting via email. I love this, and the recently released API - coupled with Olly's fast responses to feedback and adding or tweaking features - has given me the confidence to finally take the plunge and migrate this blog.</p><p>I'm also happy knowing that the blog is running on a Rails app again, which I'm far more familiar with!</p><h2 id="migrating-content">Migrating Content</h2><p>To quickly migrate the content over, I put together a simple <code>pagecord-cli</code> tool to import my markdown posts and do some <code>gsub</code>-bing to remap asset URLs.</p><p>I've also included a feature in the CLI tool can export my content back to markdown+yaml to ensure data portability.</p><h2 id="future">Future</h2><p>I really like Pagecord, and Olly has done an amazing job. Pagecord is open source, although contributions are currently closed.</p><p>However, one element I'm hoping to add are thumbnail images for my <a href="https://www.chrisblunt.com/moments/">moments posts</a>, rather than the current list of titles. </p><p>But, more than anything, it's great to support a great, independent blogging app. I hope that I'll be able to settle my blog and <em>finally</em> get back into the writing habit unencumbered by package updates, CSS tweaks, breaking changes and specs, and WYSIWYG editors.</p><p><em>PS. Yes - this post was written and sent via email 🎉</em></p>
]]>
      </description>
      <pubDate>Mon, 27 Apr 2026 23:12:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/moving-to-pagecord</link>
      <guid>https://www.chrisblunt.com/moving-to-pagecord</guid>
      <category>blog</category>
      <category>site</category>
    </item>
    <item>
      <title>Add AI's Common Passwords to your App's Blocklist</title>
      <description>
        <![CDATA[<p>Now is a good time (as any) to add common AI-generated passwords to your app's blocklist to increase security.</p>

<p>Ref: <a href="https://news.sky.com/story/are-you-using-an-ai-generated-password-it-might-be-time-to-change-it-13508611">Sky News: Are you using an AI-generated password? It might be time to change it</a></p>
]]>
      </description>
      <pubDate>Wed, 18 Feb 2026 09:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/add-ai-passwords-blocklist</link>
      <guid>https://www.chrisblunt.com/add-ai-passwords-blocklist</guid>
      <category>ai</category>
      <category>micropost</category>
      <category>security</category>
      <category>software</category>
    </item>
    <item>
      <title>2026-01-25 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/nx2bm626qrwhjb1teah7f9txi1ke" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/nx2bm626qrwhjb1teah7f9txi1ke">

    <figcaption class="attachment__caption">
      Looking through an open stone window out to a view of a rugged landscape. The sky is partly cloudy. The sea (Plymouth Sound) is visible in the far distance, along the horizon.
    </figcaption>
</figure><p>View through a stone wall looking back to Plymouth near Foggintor Quarry, Dartmoor, Devon.</p>
]]>
      </description>
      <pubDate>Sun, 25 Jan 2026 13:32:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2026-01-25-moment</link>
      <guid>https://www.chrisblunt.com/2026-01-25-moment</guid>
      <category>dartmoor</category>
      <category>devon</category>
      <category>moment</category>
    </item>
    <item>
      <title>Happy New Year - 2026</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/bs1eqy6fgtxc7kchhvbmwloim6es" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/bs1eqy6fgtxc7kchhvbmwloim6es">

    <figcaption class="attachment__caption">
      Sunset looking towards Plymouth from Saltram House (Photo taken 2025-12-13)
    </figcaption>
</figure><h1 id="happy-new-year"><strong>Happy New Year!</strong></h1><p>Welcoming 2026, here is what I plan to focus on in the months ahead. The key themes are simplification, lightness or reduction, and relying less on third-party software and services.</p><ul>
<li value="1">Develop products and productised services to complement <a href="https://www.plymouthsoftware.com/">my consulting work</a>. Simplify the offering - it feels like I've overcomplicated things. Business goals is a separate post...</li>
<li value="2">Reduce reliance on hosted services. Default to self-hosted, open-source, and plain-text-first software. See <a href="/uses/">uses</a> for progress, and my current <a href="https://bsky.app/hashtag/AppDefaults">#appdefaults</a>.</li>
<li value="3">Similarly, simplify the technology I use. Fewer gadgets. Fewer "smart" devices. Fewer apps. Fewer payments.</li>
<li value="4">Travel more, more lightly.</li>
<li value="5">Continue to work on strength training, diet, and reducing BF%.</li>
<li value="6">Read more books. Currently I am tracking books to read, those in progress, and books finished in an <a href="https://obsidian.md">Obsidian</a> note.</li>
<li value="7">Keep walking, and walk more.</li>
<li value="8">As ever - publish a bit more content here!</li>
</ul><p>See <a href="/now/">now</a> for an updating list of what I'm focussing on throughout the year.</p>
]]>
      </description>
      <pubDate>Thu, 01 Jan 2026 22:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/happy-new-year-2026</link>
      <guid>https://www.chrisblunt.com/happy-new-year-2026</guid>
      <category>goals</category>
      <category>personal</category>
    </item>
    <item>
      <title>2025-12-23 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/yvlb3cbb8nwimw1bqytmyep0nwpb" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/yvlb3cbb8nwimw1bqytmyep0nwpb">

    <figcaption class="attachment__caption">
      Disco balls hanging from the trees and reflecting Christmas lights at Crealy Theme Park, Devon, UK
    </figcaption>
</figure><p>Reflective Christmas Lights in the trees at <a href="https://www.crealy.co.uk/">Crealy</a>, Devon, UK</p>
]]>
      </description>
      <pubDate>Tue, 23 Dec 2025 16:46:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2025-12-23-moment</link>
      <guid>https://www.chrisblunt.com/2025-12-23-moment</guid>
      <category>christmas</category>
      <category>devon</category>
      <category>moment</category>
    </item>
    <item>
      <title>2025-12-21 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/oxzj9ls05moxiz04ze952rar6zi8" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/oxzj9ls05moxiz04ze952rar6zi8">

    <figcaption class="attachment__caption">
      Christmas Lights Installation, Eden Project, Cornwall, UK
    </figcaption>
</figure><p>Christmas Lights Installation @ <a href="https://www.edenproject.com">Eden Project</a>, Cornwall, UK</p>
]]>
      </description>
      <pubDate>Sun, 21 Dec 2025 17:37:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2025-12-21-moment</link>
      <guid>https://www.chrisblunt.com/2025-12-21-moment</guid>
      <category>christmas</category>
      <category>cornwall</category>
      <category>moment</category>
    </item>
    <item>
      <title>2025-08-11 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/nnn15vk3f1t86gkep7kmhtgvk7ot" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/nnn15vk3f1t86gkep7kmhtgvk7ot">

    <figcaption class="attachment__caption">
      Hamilton Sign at Theatre Royal, Plymouth
    </figcaption>
</figure><p>Hamilton @ Theatre Royal, Plymouth, UK.</p>
]]>
      </description>
      <pubDate>Mon, 11 Aug 2025 11:50:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2025-08-11-moment</link>
      <guid>https://www.chrisblunt.com/2025-08-11-moment</guid>
      <category>moment</category>
      <category>plymouth</category>
      <category>theatre</category>
    </item>
    <item>
      <title>2025-07-28 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/pwwy96yx6unrdrggrarpuus6nwlz" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/pwwy96yx6unrdrggrarpuus6nwlz">

    <figcaption class="attachment__caption">
      Ice Cream at Westward Ho! beach
    </figcaption>
</figure><p>Hockings Ice Cream, Westward Ho!, Devon, UK</p>
]]>
      </description>
      <pubDate>Mon, 28 Jul 2025 11:50:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2025-07-28-moment</link>
      <guid>https://www.chrisblunt.com/2025-07-28-moment</guid>
      <category>devon</category>
      <category>food</category>
      <category>moment</category>
    </item>
    <item>
      <title>2025-06-15 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/3kd4u8eeq9hvf0za5kr5ss9z40fm" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/3kd4u8eeq9hvf0za5kr5ss9z40fm">

    <figcaption class="attachment__caption">
      Coleton Fishacre
    </figcaption>
</figure><p>Coleton Fishacre, <a href="https://www.openstreetmap.org/way/214226273">Devon, UK</a>.</p>
]]>
      </description>
      <pubDate>Sun, 15 Jun 2025 11:50:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2025-06-15-moment-2</link>
      <guid>https://www.chrisblunt.com/2025-06-15-moment-2</guid>
      <category>devon</category>
      <category>moment</category>
    </item>
    <item>
      <title>2025-06-15 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/ysdwptytubgex0qe5c88n9rksj0e" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/ysdwptytubgex0qe5c88n9rksj0e">

    <figcaption class="attachment__caption">
      Coleton Fishacre
    </figcaption>
</figure><p>Coleton Fishacre, <a href="https://www.openstreetmap.org/way/214226273">Devon, UK</a>.</p>
]]>
      </description>
      <pubDate>Sun, 15 Jun 2025 11:20:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2025-06-15-moment-1</link>
      <guid>https://www.chrisblunt.com/2025-06-15-moment-1</guid>
      <category>coast</category>
      <category>devon</category>
      <category>moment</category>
    </item>
    <item>
      <title>2025-05-29 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/hc6xxfcwzidd7waaobxhi1myzz06" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/hc6xxfcwzidd7waaobxhi1myzz06">

    <figcaption class="attachment__caption">
      View from the queue of Nemesis Reborn Rollercoaster at Alton Towers, UK
    </figcaption>
</figure><p>Nemesis Reborn, <a href="https://www.openstreetmap.org/search?query=52.986779%2C+-1.882500&amp;zoom=19&amp;minlon=-1.884858012199402&amp;minlat=52.985787677888204&amp;maxlon=-1.8810787796974184&amp;maxlat=52.987768900008824#map=19/52.986778/-1.882499">Alton Towers, UK</a>.</p>
]]>
      </description>
      <pubDate>Thu, 29 May 2025 11:20:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2025-05-29-moment</link>
      <guid>https://www.chrisblunt.com/2025-05-29-moment</guid>
      <category>moment</category>
      <category>park</category>
      <category>rollercoaster</category>
      <category>theme</category>
    </item>
    <item>
      <title>Rails on Docker: Kamal Proxy Health Checks with Cloud Load Balancers</title>
      <description>
        <![CDATA[<p>In setting up a multi-instance development environment where multiple developers can deploy their own instance of an app to a single development VM, I stumbled across <a href="https://github.com/basecamp/kamal-proxy/discussions/103">this issue</a>.</p>

<p>Kamal Proxy is a load balancer, designed to be exposed to the Internet. However, in my client's situation, the server itself (on which Kamal Proxy was running) was behind a <a href="https://cloud.google.com/load-balancing">GCP Cloud Load Balancer</a>.</p>

<p>GCP Load Balancers (and, presumably, similar products such as <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-health-checks.html">AWS</a>) don't allow a hostname to be specified when performing a healthcheck. They ping the server on a given port, and expect an HTTP <code>200/OK</code> response.</p>

<p>If Kamal Proxy is running a single app, or an app that is running on a wildcard hostname, then this isn't a problem. However, as in this case, you may have multiple, isolated apps each with its own specific hostname, e.g:</p>

<ul>
<li><code>httpx://appname-dev-mary.example.com</code></li>
<li><code>httpx://appname-dev-john.example.com</code></li>
<li>...</li>
</ul>

<p>When the Cloud Load Balancer pinged the server without specifying a host, Kamal Proxy would respond with a 404. The result is that the server was never shown as healthy, so the Cloud Load Balancer would not route any traffic to it.</p>

<h1 id="workaround">Workaround</h1>

<p>Without a solution directly in Kamal Proxy itself, I worked around the problem by installing a small app directly into Kamal Proxy that simply returned a <code>200/OK</code> response when hit with <code>/up</code>.</p>

<p>The app uses a customised <code>nginx</code> container, which can be built use the following Dockerfile. For simplicity, you can create this on your target (deployment) server:</p>
<pre data-language="dockerfile"># Dockerfile
FROM nginx

RUN &lt;&lt;EOF
echo "server {
    listen       80;
    server_name  localhost;
    location /up {
        default_type text/plain;
    return 200 "OK";
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}" &gt; /etc/nginx/conf.d/default.conf
EOF</pre>
<p>Next, build and deploy the image into Kamal Proxy:</p>
<pre data-language="bash">$ docker build --tag nginx-healthcheck .
$ docker run -d -it --network kamal --name gcp-healthcheck-1 nginx-healthcheck
$ docker exec -it kamal-proxy kamal-proxy deploy gcp-healthcheck-1 --target gcp-healthcheck-1:80 --health-check-path "/up" --deploy-timeout 5s</pre>
<p>You can check the app is working correctly by sending a request to localhost on your server:</p>
<pre data-language="bash">$ curl http://localhost/up
# OK</pre>
<p>With the health-check app running, your Cloud Load Balancer will now see the server as healthy and start routing traffic to it.</p>


<p><strong>Note:</strong>
This is OK for internal/development servers, but probably not production.</p>
<p>As the host is not specified (<code>--host *</code>), kamal-proxy will respond to any URL that points to the server and isn't connected to an application. It will also show the server as healthy (to the Cloud Load Balancer), but the individual apps themselves might not be up and running. In that case, Kamal Proxy will return an error.</p>
]]>
      </description>
      <pubDate>Fri, 23 May 2025 10:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/kamal-proxy-health-checks-cloud-load-balancers</link>
      <guid>https://www.chrisblunt.com/kamal-proxy-health-checks-cloud-load-balancers</guid>
      <category>devops</category>
      <category>docker</category>
      <category>kamal</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>2025-05-03 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/915yj7ztz04t132kxvltfkzvvzq2" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/915yj7ztz04t132kxvltfkzvvzq2">

    <figcaption class="attachment__caption">
      Porth Beach, Cornwall
    </figcaption>
</figure><p><a href="https://www.openstreetmap.org/relation/1015668">Porth Beach</a>, Cornwall</p>
]]>
      </description>
      <pubDate>Sat, 03 May 2025 11:20:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2025-05-03-moment</link>
      <guid>https://www.chrisblunt.com/2025-05-03-moment</guid>
      <category>beach</category>
      <category>cornwall</category>
      <category>moment</category>
    </item>
    <item>
      <title>2025-04-09 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/homl4vkeoo108nyhy9uha94qq30k" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/homl4vkeoo108nyhy9uha94qq30k">

    <figcaption class="attachment__caption">
      A photo of a rocky shore with a grey boat resting on its side. A blue ocean, a wooden fence, and a distant island create a scenic landscape under clear blue skies.
    </figcaption>
</figure><p>Coastal walk near <a href="https://www.openstreetmap.org/?mlat=59.43042222222222&amp;mlon=5.254427777777778#map=19/59.430433/5.254508">Haugesund, Norway</a>.</p>
]]>
      </description>
      <pubDate>Wed, 09 Apr 2025 11:20:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2025-04-09-moment</link>
      <guid>https://www.chrisblunt.com/2025-04-09-moment</guid>
      <category>moment</category>
      <category>norway</category>
      <category>travel</category>
    </item>
    <item>
      <title>2025-04-07 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/biaxqb958rqfjpkezvg7ihp37tng" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/biaxqb958rqfjpkezvg7ihp37tng">

    <figcaption class="attachment__caption">
      A view of Fjords at Olden, Norway. The sky is bright but cloudy. On either side of the image are steep-sided mountains extending to the horizon. The mountains are capped with snow. At the bottom of the photo is water, and on the left hand side buildings can be seen in the distance on the low ground.
    </figcaption>
</figure><p>Fjords at <a href="https://www.openstreetmap.org/node/60873385">Olden, Norway</a>.</p>
]]>
      </description>
      <pubDate>Mon, 07 Apr 2025 11:20:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2025-04-07-moment</link>
      <guid>https://www.chrisblunt.com/2025-04-07-moment</guid>
      <category>fjords</category>
      <category>moment</category>
      <category>norway</category>
      <category>travel</category>
    </item>
    <item>
      <title>2025-04-06 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/5u3u9mnwimkb8dx13w2j48zk8rip" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/5u3u9mnwimkb8dx13w2j48zk8rip">

    <figcaption class="attachment__caption">
      An evening sunset overlooking the North sea. The sky colours graduate from orange through yellow to blue. In the far distance, the silhouette of two ships can be seen on the horizon. The water has small ripples and waves.
    </figcaption>
</figure><p>Sunset, North Sea</p>
]]>
      </description>
      <pubDate>Sun, 06 Apr 2025 12:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2025-04-06-moment</link>
      <guid>https://www.chrisblunt.com/2025-04-06-moment</guid>
      <category>moment</category>
      <category>sunset</category>
    </item>
    <item>
      <title>2025-02-20-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/jeqyalk739jwv0gytaq5vx9c3mr9" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/jeqyalk739jwv0gytaq5vx9c3mr9">

    <figcaption class="attachment__caption">
      A photo looking at the clock at the St Pancras Eurostar Station. Under the clock face is neon pink writing that says "I want my time with you". At the bottom right of the photo is the yellow and white front edge of a Eurostar train.
    </figcaption>
</figure><p>St Pancras, London</p>
]]>
      </description>
      <pubDate>Thu, 20 Feb 2025 15:38:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2025-02-20-moment</link>
      <guid>https://www.chrisblunt.com/2025-02-20-moment</guid>
      <category>eurostar</category>
      <category>london</category>
      <category>moment</category>
    </item>
    <item>
      <title>2025-02-17-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/otx2ui6to19bng3swszrrfmcdjfd" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/otx2ui6to19bng3swszrrfmcdjfd">

    <figcaption class="attachment__caption">
      A photo looking up at the belfry and clock tower in Lille, France. The sky is a light blue with some hazy clouds. To the left of the photo are classic buildings. A single lamp post sits in the foreground.
    </figcaption>
</figure><p>Lille, France</p>
]]>
      </description>
      <pubDate>Mon, 17 Feb 2025 15:38:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2025-02-17-moment</link>
      <guid>https://www.chrisblunt.com/2025-02-17-moment</guid>
      <category>france</category>
      <category>lille</category>
      <category>moment</category>
    </item>
    <item>
      <title>Happy New Year - 2025</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/bn9wubck3mxigpbvedhp0mcdjbq2" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/bn9wubck3mxigpbvedhp0mcdjbq2">

    <figcaption class="attachment__caption">
      Smeaton's Tower, Plymouth
    </figcaption>
</figure><p><strong>Happy New Year!</strong></p><p>Updating goals for the year ahead...</p>
]]>
      </description>
      <pubDate>Wed, 01 Jan 2025 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/happy-new-year-2025</link>
      <guid>https://www.chrisblunt.com/happy-new-year-2025</guid>
      <category>personal</category>
    </item>
    <item>
      <title>2024-12-24-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/d15azlt567drujcy57bga1kothjv" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/d15azlt567drujcy57bga1kothjv">

    <figcaption class="attachment__caption">
      A photo of rocks on a beach, looking out to sea. A small wave has just washed across the lowest rocks. To the left closest to the camera is a rock jutting out of the water. There is a similar rock further back, near the centre of the image, and in the distance a larger rock. The sky is cloudy with a slight hint of sunshine on the horizon.
    </figcaption>
</figure><p>Christmas Eve at Crownhill Bay, Devon.</p>
]]>
      </description>
      <pubDate>Tue, 24 Dec 2024 15:46:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2024-12-24-moment</link>
      <guid>https://www.chrisblunt.com/2024-12-24-moment</guid>
      <category>christmas</category>
      <category>devon</category>
      <category>moment</category>
    </item>
    <item>
      <title>2024-12-20-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/v1jpeze99e4y0r2vrpcrc2zvn21f" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/v1jpeze99e4y0r2vrpcrc2zvn21f">

    <figcaption class="attachment__caption">
      A night-time photo of Minehead beach, showing the headland in the distance. Lights can be seen on the headland, and their reflections in the water. In the foreground, the sand of the beach is illumated.
    </figcaption>
</figure><p>Minehead Beach, Somerset.</p>
]]>
      </description>
      <pubDate>Fri, 20 Dec 2024 18:30:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2024-12-20-moment</link>
      <guid>https://www.chrisblunt.com/2024-12-20-moment</guid>
      <category>cornwall</category>
      <category>eden-project</category>
      <category>moment</category>
    </item>
    <item>
      <title>Simplifying Business with Automation with Zapier</title>
      <description>
        <![CDATA[<p><img src="%7B%7B%20%22https://assets.chrisblunt.com/2024/12/email-kit-zapier-workflow.png%22%20%7C%20image_options:%20%22format=webp,width=780%22%20%7D%7D" alt="Screenshot of Zapier workflow showing flow from email to convertkit"></p>

<p>One of the things I've noticed running <a href="https://www.plymouthsoftware.com">Plymouth Software</a> over the years is just how quick and easy it is to over-complicate everyday tasks.</p>

<p>As business owners, there is a vast (<em>vast!</em>) number of apps and services vying for our attention - and curiosity leads me to want to try each of them.</p>

<p>Over months and years, all these things build up until eventually we spend more time navigating between them, learning new interfaces, etc. than actually creating anything…</p>

<p>For this reason, I've spent a little time recently trying to cut complexities out of my business, and settle on using a few simple tools and approaches.</p>

<p>One of the most important services I am using to do this is <a href="https://www.zapier.com/">Zapier</a>, a "no/low-code" application to connect various systems together. With a small amount of custom code to tidy up text and formatting, Zapier can be used to build powerful event-driven processes into your business.</p>

<h2 id="email-as-the-interface">Email as the Interface</h2>

<p>As an example, this post was written in my normal email app. I love the simplicity of email-as-the-interface, and wanted to adopt the approach for my mailing lists. I currently use <a href="https://www.kit.com/">(Convert) Kit</a>, but I find the content editor creates too much friction. I wondered if I could instead write posts in my normal email UI, but still publish them through Kit.</p>

<p>So I created a Zapier automation - or <em>Zap</em> - to do just this. I send the email to a special, unique address created by Zapier. It then parses the content, using a little custom code to do tweak the content so it meets Kit's requirements, and then creates the email draft via their API.</p>

<h2 id="future-improvements">Future improvements</h2>

<p>Currently, the automation stops there, leaving the email as a draft in my Kit account. This lets me tweak the audience segments, schedule publication date, and so on.</p>

<p>Once Zapier updates the Kit integration to support the V4 API, this is something that I hope to fully automate.</p>
]]>
      </description>
      <pubDate>Mon, 16 Dec 2024 13:22:53 +0000</pubDate>
      <link>https://www.chrisblunt.com/simplifying-business-automation-zapier</link>
      <guid>https://www.chrisblunt.com/simplifying-business-automation-zapier</guid>
      <category>business</category>
      <category>nocode</category>
      <category>post</category>
    </item>
    <item>
      <title>2024-12-15-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/rvqf245mnht19hfkys1ggsvjoubw" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/rvqf245mnht19hfkys1ggsvjoubw">

    <figcaption class="attachment__caption">
      A photo of purple/blue laser lights in all directions, emanating from a central point. Taken at Eden Project Christmas Lights Experience, Cornwall.
    </figcaption>
</figure><p>Christmas Lights Experience at <a href="https://www.edenproject.com/">Eden Project</a>, Cornwall.</p>
]]>
      </description>
      <pubDate>Sun, 15 Dec 2024 18:30:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2024-12-15-moment</link>
      <guid>https://www.chrisblunt.com/2024-12-15-moment</guid>
      <category>cornwall</category>
      <category>eden-project</category>
      <category>moment</category>
    </item>
    <item>
      <title>2024-12-08-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/8v35n31p0ql7sizotc2qu4z21bq4" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/8v35n31p0ql7sizotc2qu4z21bq4">

    <figcaption class="attachment__caption">
      A photo of a small river edged by trees. Between the trees is a blue sky. A short distance away, through the trees, an old mill house can be seen.
    </figcaption>
</figure><p><a href="https://www.nationaltrust.org.uk/visit/cornwall/cotehele-mill">Cotehill Mill</a>, Cornwall.</p>
]]>
      </description>
      <pubDate>Sun, 08 Dec 2024 15:20:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2024-12-08-moment</link>
      <guid>https://www.chrisblunt.com/2024-12-08-moment</guid>
      <category>cornwall</category>
      <category>moment</category>
      <category>national-trust</category>
      <category>river</category>
    </item>
    <item>
      <title>2024-11-21-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/9jff1za3ybrlkt1ttbu69etspy74" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/9jff1za3ybrlkt1ttbu69etspy74">

    <figcaption class="attachment__caption">
      An evening photo across moors on Dartmoor National Park, Devon. The ground is covered in snow. Small patches of grass and gorse can be seen breaking through the snow. The sky has light grey cloud. On the horizon is a light pink/yellow glow from the sunset.
    </figcaption>
</figure><p>Snow skies over Dartmoor, Devon.</p>
]]>
      </description>
      <pubDate>Thu, 21 Nov 2024 15:20:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2024-11-21-moment</link>
      <guid>https://www.chrisblunt.com/2024-11-21-moment</guid>
      <category>cornwall</category>
      <category>moment</category>
      <category>sunset</category>
    </item>
    <item>
      <title>2024-10-27-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/edtorp09kbqk0pnzijbc860co9io" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/edtorp09kbqk0pnzijbc860co9io">

    <figcaption class="attachment__caption">
      Sunset over a a grey gravel path surrounded by shrubs. Small boulders edge the path.
    </figcaption>
</figure><p>Sunset walk at <a href="https://www.edenproject.com/">Eden Project</a>, Cornwall.</p>
]]>
      </description>
      <pubDate>Sun, 27 Oct 2024 15:20:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2024-10-27-moment</link>
      <guid>https://www.chrisblunt.com/2024-10-27-moment</guid>
      <category>cornwall</category>
      <category>moment</category>
      <category>sunset</category>
    </item>
    <item>
      <title>2024-10-11-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/vxiazh1t33zak5nptn3ef3l0sev8" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/vxiazh1t33zak5nptn3ef3l0sev8">

    <figcaption class="attachment__caption">
      Red takeaway coffee cup on grey table.
    </figcaption>
</figure><p>Morning Coffee (and code) - <a href="https://www.nuffieldhealth.com/gyms/devonshire-plymouth">Nuffield Devonshire</a>.</p>
]]>
      </description>
      <pubDate>Fri, 11 Oct 2024 18:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-10-11-moment</link>
      <guid>https://www.chrisblunt.com/2024-10-11-moment</guid>
      <category>coffee</category>
      <category>moment</category>
    </item>
    <item>
      <title>2024-09-28-moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/f7mtlf86v7k5u1fla75y19plztx6" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/f7mtlf86v7k5u1fla75y19plztx6">

    <figcaption class="attachment__caption">
      Lydford Gorge, Devon
    </figcaption>
</figure><p>Sunday afternoon walk along the river at <a href="https://www.nationaltrust.org.uk/visit/devon/lydford-gorge">Lydford Gorge</a>, Devon.</p>
]]>
      </description>
      <pubDate>Sat, 28 Sep 2024 18:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-09-28-moment</link>
      <guid>https://www.chrisblunt.com/2024-09-28-moment</guid>
      <category>devon</category>
      <category>moment</category>
    </item>
    <item>
      <title>Rails on Docker: Build Secrets for Yarn with 1Password</title>
      <description>
        <![CDATA[<p>Whilst moving an existing Rails project to Docker, I've been making use of multi-stage builds to optimise the image creation process. For this project, some of the npm/yarn packages were hosted in a private git repository. The <code>package.json</code> looked something like:</p>
<pre data-language="json">{
  "license": "UNLICENSED",
  "private": true,
  "dependencies": {
    // ...
    "package-a": "github:cblunt/package-a#main",
    "package-b": "github:cblunt/package-b#main1",
    // ...
  }</pre>
<p>When building the image, the <code>yarn install</code> step attempts to download these packages, but requires the use of my host machine's private SSH key to authenticate and connect to the Github repositories.</p>

<p>Luckily, Docker has introduced the concept of build-secrets to handle just this situation. Build-secrets allow us to mount environment variables temporarily into the build process, discarding them afterwards so that they are not present in any of the image layers.</p>

<p>Furthermore, there is a special SSH build-secret that forwards the host's SSH agent into the image. In the <code>Dockerfile</code>, this can be achieved using the <code>--mount=type=ssh</code> argument:</p>
<pre data-language="dockerfile">RUN --mount=type=ssh mkdir -p ~/.ssh &amp;&amp; \
    ssh-keyscan github.com &gt; ~/.ssh/known_hosts &amp;&amp; \
    yarn install &amp;&amp; \
    rm -rf ~/.ssh</pre>

<strong>Warning</strong>
In this example, we are trusting that <code>github.com</code> correctly resolves to the IP address of Github.


<p>The docker image can then be built by passing adding your key to the local SSH agent, and forwarding it into the build (using the identifier <code>default</code> which defaults to the value of the <code>SSH_AUTH_SOCK</code> environment variable):</p>
<pre data-language="bash">$ eval $(ssh-agent)
$ ssh-add ~/.ssh/id_ed25519 # path to your private SSH keyfile
# Identity added: ...

$ docker buildx build --ssh default .</pre>
<p>Docker will now build the image, temporarily using the forwarded SSH credentials to connect to Github and download packages from private repositories.</p>

<h2 id="1password-managed-credentials">1Password Managed Credentials</h2>

<p>However, I use 1Password to manage my SSH keys and act as the SSH agent. This means that the private keyfiles (<code>id_ed25519</code>, <code>id_rsa</code>, etc.) are not saved into my home folder.</p>

<p>As 1Password runs as the SSH agent on my machine, instead of mounting the SSH<em>AUTH</em>SOCK, I needed to instead pass the socket file that 1Password creates, the location of which can be found in their <a href="https://developer.1password.com/docs/ssh/get-started/#step-4-configure-your-ssh-or-git-client">documentation</a>.</p>

<p>With this, we can skip adding the key to the default SSH agent, and instead make use of 1Password's agent:</p>
<pre data-language="bash">$ docker buildx build --ssh default==$HOME/Library/Group\ Containers/2BUA8C4S2C.com.1password/t/agent.sock .</pre>
<p>Docker will now build the image, using 1Password's SSH agent to forward the private key into the build process. This keeps the key securely stored in 1Password.</p>

<p>The same argument can also be passed when using Docker Compose:</p>
<pre data-language="bash">$ docker compose build --ssh default=$HOME/Library/Group\ Containers/2BUA8C4S2C.com.1password/t/agent.sock</pre>
<h2 id="passing-github-tokens-via-1password">Passing Github Tokens via 1Password</h2>

<p>Similarly, I use 1Password's <code>gh</code> plugin to manage my Github access token.</p>

<p>In the same way as SSH secrets can be mounted, Docker can also forward other secrets into the build process that are stored in environment variables.</p>

<p>For example, to pass your Github token into the build downloading gems from Github:</p>
<pre data-language="dockerfile">RUN --mount=type=secret,id=github_token BUNDLE_GITHUB__COM="x-access-token:$(cat /run/secrets/github_token)" \
    bundle install &amp;&amp; \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git</pre>
<p>Note that the secrets are accessed like a text file, so to get their value in the Dockerfile, we use <code>cat</code> to output the value of the secret: <code>cat /run/secrets/github_token</code>.</p>

<p>To pass this value into Docker:</p>
<pre data-language="bash">$ docker buildx build --secret id=github_token,env=GITHUB_TOKEN .</pre>
<p>Using the 1Password, <code>op</code> command, these values can retrieved and passed into the build:</p>
<pre data-language="bash">$ GITHUB_TOKEN=$(op read "op://Vault/Github Token Item Name/token") \
    docker buildx build --secret id=github_token,env=GITHUB_TOKEN .</pre>
<p>Similarly, the token can be configured in Docker Compose:</p>
<pre data-language="yaml">services:
  # ...

secrets:
  github_token:
    environment: GITHUB_TOKEN</pre>
<p>It can then be read when running <code>docker compose build</code> by setting the environment variable:</p>
<pre data-language="bash">$ GITHUB_TOKEN=$(op read "op://Vault/Github Token Item Name/token") \
    docker compose build</pre>
<p>Using this approach, it is possible to inject both SSH and token secrets managed by 1Password into your Docker build process, while maintaining the benefit of Docker's build secrets.</p>
]]>
      </description>
      <pubDate>Mon, 23 Sep 2024 12:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-docker-build-secrets-yarn-1password-github</link>
      <guid>https://www.chrisblunt.com/rails-docker-build-secrets-yarn-1password-github</guid>
      <category>1password</category>
      <category>docker</category>
      <category>github</category>
      <category>howto</category>
      <category>npm</category>
      <category>rails</category>
      <category>tutorial</category>
      <category>yarn</category>
    </item>
    <item>
      <title>2024-09-12 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/dz3o9lwk44w3zg8buq9fjkg3gt1j" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/dz3o9lwk44w3zg8buq9fjkg3gt1j">

    <figcaption class="attachment__caption">
      Sutton Harbour, Plymouth.
    </figcaption>
</figure><p>Sutton Harbour, Plymouth.</p>
]]>
      </description>
      <pubDate>Thu, 12 Sep 2024 10:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-09-12-moment</link>
      <guid>https://www.chrisblunt.com/2024-09-12-moment</guid>
      <category>devon</category>
      <category>harbour</category>
      <category>moment</category>
      <category>plymouth</category>
    </item>
    <item>
      <title>2024-08-18 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/dbxfwl4ozcg7fx6q0942i9f1ecvw" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/dbxfwl4ozcg7fx6q0942i9f1ecvw">

    <figcaption class="attachment__caption">
      Ferry Meadows, Peterborough
    </figcaption>
</figure><p>Afternoon walk around the lakes at <a href="https://www.nenepark.org.uk/ferry-meadows">Ferry Meadows</a>.</p>
]]>
      </description>
      <pubDate>Sun, 18 Aug 2024 17:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-08-18-moment</link>
      <guid>https://www.chrisblunt.com/2024-08-18-moment</guid>
      <category>cambridgeshire</category>
      <category>lake</category>
      <category>moment</category>
      <category>travel</category>
    </item>
    <item>
      <title>2024-08-14 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/n1288p1b2d052mh6233agjktn8eu" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/n1288p1b2d052mh6233agjktn8eu">

    <figcaption class="attachment__caption">
      Coffee on Table
    </figcaption>
</figure><p>Coffee meet with <a href="https://www.nimbility.co/">Glenn</a> at Rusty Tractor Farm Cafe, Devon.</p>
]]>
      </description>
      <pubDate>Wed, 14 Aug 2024 16:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-08-14-moment</link>
      <guid>https://www.chrisblunt.com/2024-08-14-moment</guid>
      <category>coffee</category>
      <category>cream</category>
      <category>devon</category>
      <category>moment</category>
      <category>tea</category>
    </item>
    <item>
      <title>2024-07-29 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/o9ldfhbj1rlgj2cuddfmbty6aksf" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/o9ldfhbj1rlgj2cuddfmbty6aksf">

    <figcaption class="attachment__caption">
      Weston Super Mare, Somerset
    </figcaption>
</figure><p>Weston Super Mare, Somerset.</p>
]]>
      </description>
      <pubDate>Mon, 29 Jul 2024 12:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-07-29-moment</link>
      <guid>https://www.chrisblunt.com/2024-07-29-moment</guid>
      <category>beach</category>
      <category>mare</category>
      <category>moment</category>
      <category>north</category>
      <category>pier</category>
      <category>somerset</category>
      <category>super</category>
      <category>weston</category>
    </item>
    <item>
      <title>Rails: Simplifying Local Environment Setup</title>
      <description>
        <![CDATA[<p><img src="https://assets.chrisblunt.com/2024/07/2024-07-15-setup-script.webp" alt="Terminal screenshot showing bin/setup"></p>

<p>Rails includes a default script, <code>bin/setup</code>, that runs some basic environment setup such as installing gems, creating and seeding the database, and so on.</p>

<p>Often, though, any further environment setup, such as configuring default user accounts, installing supporting services, and so on, is described in a project's <code>README</code>. With time, this document grows and the installation process becomes more complicated, often including lots of copy-and-paste scripts, manual file edits, and so on.</p>

<p>I'm working on upgrading a legacy application that had used this approach, and so spent some time consolidating the various setup requirements and configurations into an automated <code>bin/setup</code> script.</p>

<p>The result was that the full local development stack, including supporting Docker images, became a simple one-line command:</p>
<pre data-language="bash">$ bin/setup</pre>
<p>The benefits of such a small investment can be huge, especially when onboarding new members to your dev team, or simply spinning up a new working copy of the application for yourself.</p>

<p>Keeping the setup in a single command also makes it simpler to provision working environments in services such as Github Codespaces.</p>
]]>
      </description>
      <pubDate>Wed, 17 Jul 2024 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-simplifying-local-environment-setup</link>
      <guid>https://www.chrisblunt.com/rails-simplifying-local-environment-setup</guid>
      <category>healthyrailsapps</category>
      <category>rails</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>2024-06-22 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/pdr9iu9838wrd5ky6tlcwxyhvacv" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/pdr9iu9838wrd5ky6tlcwxyhvacv">

    <figcaption class="attachment__caption">
      Westward Ho!, North Devon
    </figcaption>
</figure><p>Westward Ho!, North Devon.</p>
]]>
      </description>
      <pubDate>Sat, 22 Jun 2024 18:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-06-22-moment</link>
      <guid>https://www.chrisblunt.com/2024-06-22-moment</guid>
      <category>coast</category>
      <category>devon</category>
      <category>moment</category>
      <category>north</category>
      <category>westward</category>
    </item>
    <item>
      <title>Rails 7: Default Dockerfile for Alpine Linux</title>
      <description>
        <![CDATA[<p>Getting started with Rails on Docker has been made much easier with Rails 7.1, with the inclusion of a default <code>Dockerfile</code>. However, the default image that's built results in a large image (~730MB for a demo Rails app I spun up).</p>

<p>One of the benefits of the Alpine Linux images is that they are very minimal, resulting in much smaller images for the same application.</p>

<p>So, I set about rewriting the default <code>Dockerfile</code> that's generated by Rails to use the <code>ruby:3.3.1-alpine3.18</code> image, and discovered a few trip-hazards along the way.</p>

<h2 id="getting-started">Getting Started</h2>

<p>In this tutorial, we'll spin up a default Rails app (using SQLite as a database) and update the <code>Dockerfile</code> and requirements to use Alpine. To get started, create a new app and the traditional blog-post scaffold:</p>
<pre data-language="bash"># versions: ruby = 3.3.1, rails ~&gt; 7.1.3
$ rails new demo-app
$ cd demo-app
$ bin/rails g scaffold post title:string body:text
$ bin/rails db:migrate
$ bin/rails s</pre>
<p>Open <a href="http://localhost:3000/posts">localhost:3000/posts</a> to ensure that everything is running smoothly. At this stage, we can also create a new <code>docker-compose.yml</code> file to simplify building images and creating containers:</p>
<pre data-language="yaml"># docker-compose.yml
services:
  app:
    build: .
    platform: linux/amd64
    environment:
      - RAILS_ENV=development
    ports:
      - 3000:3000
    volumes:
      - .:/rails
    command: ["bin/rails", "server", "-b", "0.0.0.0"]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/healthz"]
      interval: 30s
      timeout: 10s
      retries: 5</pre>
<p>We can now build the default image for the application using docker compose, and see the resulting image is around 730MB in size.</p>
<pre data-language="bash">$ docker compose build
$ docker images

# REPOSITORY     TAG     ...     SIZE
# demo-app-app   latest  ...     730MB</pre>
<h2 id="switching-to-alpine">Switching to Alpine</h2>

<p>Alpine Linux is a lightweight linux distribution, making it well-suited to Docker images.</p>

<p>To switch our application to use Alpine instead of the default Debian-based <code>ruby</code> image, we need to make some changes to <code>Dockerfile</code> base image, and the packages:</p>
<pre data-language="diff"># Dockerfile
- FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
+ FROM registry.docker.com/library/ruby:$RUBY_VERSION-alpine3.18 as base

  # Install packages needed to build gems
- RUN apt-get update -qq &amp;&amp; \
-    apt-get install --no-install-recommends -y build-essential git libvips pkg-config
+ RUN apk add --update --no-cache build-base git vips sqlite-libs tzdata

  # Install packages needed for deployment
- RUN apt-get update -qq &amp;&amp; \
-    apt-get install --no-install-recommends -y curl libsqlite3-0 libvips &amp;&amp; \
-    rm -rf /var/lib/apt/lists /var/cache/apt/archives
+ RUN apk add --update --no-cache curl vips sqlite
# ...

# Run and own only the runtime files as a non-root user for security
- RUN useradd rails --create-home --shell /bin/bash &amp;&amp; \
+ RUN adduser rails --disabled-password --shell /bin/ash &amp;&amp; \</pre>
<strong>Why not use <code>alpine3.20</code>?</strong> At the time of writing, there is a <a href="https://github.com/sparklemotion/sqlite3-ruby/issues/434">conflict</a> when using the ActiveRecord SQLite adapter, and sqlite available in alpine 3.19+. I will update this post once this is fixed, but there are some workarounds you can try in the issue tracker.

<p>We also need to make a slight change to the default <code>docker-entrypoint</code> file, as <code>bash</code> is not available in Alpine. We'll use the default <code>ash</code> shell instead:</p>
<pre data-language="diff"># /rails/bin/docker-entrypoint
- #!/bin/bash -e
+ #!/bin/ash -e</pre>
<p>With the files updated, you can now build the new image and see the reduced image size (in this case, a reduction of around 69.5%):</p>
<pre data-language="bash">$ docker compose build
$ docker images

# REPOSITORY     TAG     ...     SIZE
# demo-app-app   latest  ...     223MB</pre>
<p>Next, restart your container using the new image:</p>
<pre data-language="bash">$ docker compose up -d
$ open http://localhost:3000/posts/</pre>
<p>You should now be presented with the posts index. Note that because our app is using the default sqlite3 database, the database file (<code>storage/development.sqlite3</code>) has been copied into the container. This is because the local directory is mounted into the container by Docker Compose.</p>

<p>For production, however, these database files won't be present. You can simulate this by removing the bind mount from <code>docker-compose.yml</code>:</p>
<pre data-language="yaml"># docker-compose.yml
services:
  app:
    # ...
    # volumes:     &lt;- comment out these lines
    #  - .:/rails  &lt;- </pre>
<p>Now recreate your Docker Compose stack:</p>
<pre data-language="bash">$ docker compose up -d
$ open http://localhost:3000/</pre>
<p>In development mode, you will now see the Pending Migration error, with an option to run the migration. Click the button to run the pending migration, and your app will continue to operate as normal.</p>

<p>Remember that when you remove your container (e.g. with <code>docker compose down</code>), then the database file will also be deleted. For this reason, it is recommended to revert the previous change to <code>docker-compose.yml</code>, which is fine for development and testing:</p>
<pre data-language="yaml"># docker-compose.yml
services:
  app:
    # ...
    volumes:
     - .:/rails</pre><pre data-language="bash">$ docker compose up -d</pre>
<p>All done! Your rails app is now running on the much smaller Alpine linux base image.</p>
]]>
      </description>
      <pubDate>Mon, 03 Jun 2024 14:10:21 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-7-default-dockerfile-alpine</link>
      <guid>https://www.chrisblunt.com/rails-7-default-dockerfile-alpine</guid>
      <category>docker</category>
      <category>howto</category>
      <category>rails</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>2024-05-29 Moment 2</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/ns331mw1f2v4buwab48sn7kzivud" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/ns331mw1f2v4buwab48sn7kzivud">

    <figcaption class="attachment__caption">
      Eiffel Tower, Paris.
    </figcaption>
</figure><p>Eiffel Tower, Paris.</p>
]]>
      </description>
      <pubDate>Wed, 29 May 2024 18:01:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-05-29-moment-2</link>
      <guid>https://www.chrisblunt.com/2024-05-29-moment-2</guid>
      <category>france</category>
      <category>moment</category>
      <category>paris</category>
    </item>
    <item>
      <title>2024-05-29 Moment 1</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/b2zqp0fbydusce49yf6onf17x72a" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/b2zqp0fbydusce49yf6onf17x72a">

    <figcaption class="attachment__caption">
      Louvre, Paris
    </figcaption>
</figure><p>Louvre, Paris.</p>
]]>
      </description>
      <pubDate>Wed, 29 May 2024 18:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-05-29-moment-1</link>
      <guid>https://www.chrisblunt.com/2024-05-29-moment-1</guid>
      <category>france</category>
      <category>moment</category>
      <category>paris</category>
    </item>
    <item>
      <title>2024-05-28 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/xm2dipqtkgd6rm5thup5qyt1biqm" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/xm2dipqtkgd6rm5thup5qyt1biqm">

    <figcaption class="attachment__caption">
      Disneyland Paris
    </figcaption>
</figure><p>Disneyland Paris, France.</p>
]]>
      </description>
      <pubDate>Tue, 28 May 2024 18:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-05-28-moment</link>
      <guid>https://www.chrisblunt.com/2024-05-28-moment</guid>
      <category>disney</category>
      <category>france</category>
      <category>moment</category>
      <category>paris</category>
    </item>
    <item>
      <title>Updating Millions of Rows in PostgreSQL with Parallel Database Clusters</title>
      <description>
        <![CDATA[<p>For a large database transform and migration project back in November, the requirement arose to populate a column in 100+ million records with values referencing across multiple other tables in different schemas.</p>

<p>I wrote a <a href="https://www.postgresql.org/docs/current/plpgsql.html">Pl/pgSQL</a> function to perform the lookup, and - as the value would be reused across a batch of 100-or-so rows per lookup - insert the results of the lookup into a cache table.</p>

<p>The function looked something like this:</p>
<pre data-language="sql">-- Example function, not the real code...
CREATE FUNCTION get_document_title(userid int)
  RETURNS TABLE(user_id int, document_label text)
  LANGUAGE plpgsql
AS $$
DECLARE
  cacheresult record;
  result record;
BEGIN

--
-- Check cache
SELECT COUNT(*) INTO cacheresult 
  FROM cache_table WHERE user_id = userid;

IF cacheresult &gt; 0
THEN
  --
  -- Result has been cached
  RETURN QUERY
    SELECT user_id, document_label
      FROM cache_table 
      WHERE user_id = userid
      LIMIT 1;
ELSE
  --
  -- Perform the lookup. 
  -- In the real code, there are actually several branches for looking up from different sources, but for this demo, I've simplified:
  SELECT * INTO result
    FROM documents 
    WHERE user_id = userid 
      AND document_type_id = doctypeid 
      AND project_id IN (
        SELECT id 
          FROM projects 
          WHERE archived IS NOT TRUE
      ) 
    LIMIT 1;

  --
  -- Cache the result
  INSERT INTO cache_table (user_id, document_label) 
    VALUES (userid, result.document_label);

  --
  -- Return the result
  RETURN QUERY
    SELECT document_label
      FROM result;
END IF;
END;
$$</pre>
<p>However, with over 100m rows to process, the resulting query was projected to run into days - if not <em>weeks</em> - of processing time. This was running on a fairly large Aurora PG cluster.</p>

<p>Whilst <a href="https://www.chrisblunt.com/outdoors/">on a walk</a> and not thinking about this problem, the idea suddenly occurred to me of splitting the process across parallel database clusters.</p>

<p>The source table had already been partitioned by year in an attempt to speed things up, but as each row was being updated in sequence, the time required was still too long.</p>

<p>So I set about using RDS to clone the source database - one for each year partition - and then starting the <code>UPDATE</code> individually on each cluster using a small bash command:</p>
<pre data-language="bash">$ echo "UPDATE partition_table_2015 SET document_title = get_document_label(user_id) WHERE ..." | psql -h dbcluster_1;</pre>
<p>Similarly, on the second DB cluster:</p>
<pre data-language="bash">$ echo "UPDATE partition_table_2016 SET document_title = get_document_label(user_id) WHERE ..." | psql -h dbcluster_2;</pre>
<p>...and so on.</p>

<p>With each DB cluster having its own resources (and writer instance), the plan was that this would drastically cut down the number of hours required to process the full table.</p>

Since the time of writing, AWS Aurora has released <a href="https://aws.amazon.com/about-aws/whats-new/2019/08/amazon-aurora-multimaster-now-generally-available/">Multi-Master</a> which might offer an alternative solution to spinning up multiple clusters.

<p>As each DB cluster finished updating its partition, the partition table was exported via <code>pg_dump</code> and then reimported back into the primary DB cluster:</p>
<pre data-language="bash"># Export partition from each individual DB cluster:
$ pg_dump -Fc -h dbcluster_1 --table partition_table_2015 partition_2015.pgdump
$ pg_dump -Fc -h dbcluster_2 --table partition_table_2016 partition_2016.pgdump
$ pg_dump -Fc -h dbcluster_3 --table partition_table_2017 partition_2017.pgdump

# Import all the exports back into the Primary DB cluster:
$ pg_restore -h db_primary /tmp/partition_2015.pgdump
$ pg_restore -h db_primary /tmp/partition_2016.pgdump
$ pg_restore -h db_primary /tmp/partition_2017.pgdump</pre>
<p>Whilst this process still took some time, parallelising the work across multiple DB clusters definitely sped things up, and meant that each partition could be independently verified before removing the cluster and importing the <code>pgdump</code> file back into the main DB cluster.</p>
]]>
      </description>
      <pubDate>Thu, 16 May 2024 14:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/postgres-updates-parallel-database-clusters</link>
      <guid>https://www.chrisblunt.com/postgres-updates-parallel-database-clusters</guid>
      <category>database</category>
      <category>postgresql</category>
      <category>software</category>
    </item>
    <item>
      <title>2024-05-06 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/knliq3fdgxq4k87jay5eqnt4tldn" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/knliq3fdgxq4k87jay5eqnt4tldn">

    <figcaption class="attachment__caption">
      Looe, Cornwall
    </figcaption>
</figure><p>Evening at the beach and waterside in Looe, Cornwall.</p>
]]>
      </description>
      <pubDate>Mon, 06 May 2024 10:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/2024-05-06-moment</link>
      <guid>https://www.chrisblunt.com/2024-05-06-moment</guid>
      <category>cornwall</category>
      <category>looe</category>
      <category>moment</category>
    </item>
    <item>
      <title>Trying Note to Self App</title>
      <description>
        <![CDATA[<p>I've been trying out <a href="https://play.google.com/store/apps/details?id=com.makenotetoself">notetoselfapp</a> for Android recently. It ticks nearly every box I've looked for in a quick-capture notes app for mobile since leaving Evernote. </p>

<p>Note to Self has a chat-style UI, and while there is no data sync, it has SQLite export for data portability. </p>

<p>I'm hopeful that Markdown rendering is on the roadmap...</p>
]]>
      </description>
      <pubDate>Fri, 26 Jan 2024 13:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/trying-out-notes-to-self-app</link>
      <guid>https://www.chrisblunt.com/trying-out-notes-to-self-app</guid>
      <category>personal</category>
      <category>productivity</category>
      <category>thoughts</category>
    </item>
    <item>
      <title>2024-01-11 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/2ltjev5gvsp3ln79pczvbiijj47r" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/2ltjev5gvsp3ln79pczvbiijj47r">

    <figcaption class="attachment__caption">
      Sutton Harbour, Plymouth
    </figcaption>
</figure><p>City Conversations with <a href="https://devonchamber.co.uk/">Devon and Plymouth Chamber of Commerce</a> at Sutton Harbour, Plymouth.</p>
]]>
      </description>
      <pubDate>Thu, 11 Jan 2024 08:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2024-01-11-moment</link>
      <guid>https://www.chrisblunt.com/2024-01-11-moment</guid>
      <category>devon</category>
      <category>harbour</category>
      <category>moment</category>
      <category>plymouth</category>
    </item>
    <item>
      <title>2024-01-07 Moment</title>
      <description>
        <![CDATA[<figure class="attachment attachment--preview attachment--jpeg">
      <img alt="Uploaded image" data-lightbox-full-url="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/v9oucb65zgr1dgj57xjnfk9oie01" src="https://pagecord.com/cdn-cgi/image/width=1600,height=1200,format=webp,quality=90/https://storage.pagecord.com/v9oucb65zgr1dgj57xjnfk9oie01">

    <figcaption class="attachment__caption">
      Sharpitor, Dartmoor, Devon
    </figcaption>
</figure><p>🌲 Winter Sunday walk across Dartmoor.</p>
]]>
      </description>
      <pubDate>Sun, 07 Jan 2024 15:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2024-01-07-moment</link>
      <guid>https://www.chrisblunt.com/2024-01-07-moment</guid>
      <category>dartmoor</category>
      <category>devon</category>
      <category>moment</category>
      <category>sharpitor</category>
    </item>
    <item>
      <title>Happy New Year - 2024 Edition!</title>
      <description>
        <![CDATA[<p><img src="https://assets.chrisblunt.com/2024/01/sunset-behind-trees.webp" alt="Sunset behind trees"></p>

<p><strong>Happy New Year!</strong></p>

<p>I can't believe it's been a year since my <a href="/happy-new-year-2023/">last post</a>! My hope of regular posting to this blog was dashed by a year of ongoing <del>frustration</del> learning on AWS for a large data migration and transformation project, amongst other client projects.</p>

<p>The silver lining is I've now gained a much better understanding of how the various services work together, the approach that's necessary for large data transform projects, and the (many, many) hoops that must be jumped through to get various AWS services such as DMS, Glue, Athena, Lambda, SQS, and RDS working together.</p>

<p>This has necessitated a lot of working with serverless tools and micro-service architectures, and managing <em>so much</em> service infrastructure to achieve desired outcomes.</p>

<p>In my experience, this new approach might have solved some problems, but has certainly introduced new ones into the mix.</p>

<p>This experience has only re-affirmed my resolve to stick with the simplicity of a "all-in", conventions-based framework such as Rails, coupled with managed infrastructure such as AppRunner or Digital Ocean's App Platform.</p>

<p>At the same time, I've been happy to see an apparent "resurgence" in Rails (and other full-stack) frameworks over 2023, and am excited to see the developments in recent and <a href="https://github.com/rails/rails/milestone/87">upcoming releases</a> of Rails.</p>

<p>So with that in mind, I am updating my <a href="https://www.plymouthsoftware.com">consulting services</a> and developing a couple of new products for 2024. All being well, I'll post updates here (at a better rate than last year!) as they are released.</p>

<p>Wishing you a happy, healthy, and successful 2024.</p>
<pre data-language="ruby">current_year += 1

goals = %i[
  :write_more_articles,
  :update_rails_courses,
  :travel,
  :new_products,
]

goals.map do |g|
  # ...
end</pre>
]]>
      </description>
      <pubDate>Mon, 01 Jan 2024 12:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/happy-new-year-2024</link>
      <guid>https://www.chrisblunt.com/happy-new-year-2024</guid>
      <category>personal</category>
    </item>
    <item>
      <title>Happy New Year</title>
      <description>
        <![CDATA[<p><strong>Wishing you a Happy New Year!</strong></p>

<p>I hope that your 2023 is happy, healthy, and successful.</p>
<pre data-language="javascript">currentYear++

let goals = new Array()
// TODO: ...</pre>
]]>
      </description>
      <pubDate>Sun, 01 Jan 2023 12:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/happy-new-year-2023</link>
      <guid>https://www.chrisblunt.com/happy-new-year-2023</guid>
      <category>personal</category>
    </item>
    <item>
      <title>Getting Ready for Goals</title>
      <description>
        <![CDATA[<p>The time of year is approaching when goals and habits are due to be set. As the end of 2022 approaches, I'm happy to report that one of my goals - to improve overall health and fitness - <em>was</em> going quite well.</p>

<p>Up until last week, that is, when I heard myself uttering the phrase "Oh, it's Christmas" on multiple occassions when a <del>excuse</del> reason was needed for eating something not-traditionally-thought-of as good for you; or was needed to avoid some form of exercise. My Fitbit is begining to wonder if it's been retired.</p>

<p>So in a haze of coffee*-fuelled enthusiasm, I'm starting to plan out what my goals for 2023 will look like. As ever, the world of software is changing very rapidly, so as well as personal goals, it's also a good time to figure out what <a href="https://www.plymouthsoftware.com">Plymouth Software</a> will be for the months and years ahead.</p>

<p>* Topped with cream, of course. Because it's Christmas.</p>
]]>
      </description>
      <pubDate>Tue, 27 Dec 2022 15:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/getting-ready-for-goals</link>
      <guid>https://www.chrisblunt.com/getting-ready-for-goals</guid>
      <category>100days</category>
      <category>business</category>
      <category>goals</category>
      <category>life</category>
    </item>
    <item>
      <title>Indecision</title>
      <description>
        <![CDATA[<p>As well as my general notes capture tool, I've recently been cycling through different ways to manage this blog, trying to optimise my writing flow.</p>

<p>More often than not, I'm distracted by the wealth of tools that are available, such that I end up procrastinating over those more than actually writing!</p>

<p>This is something I struggle with in running business as well - there are always so many options and things-to-be-done, that it is far too easy to get stuck very much in the weeds, postponing decisions that would be much better made and forgotten about.</p>

<p>I'm sure technology was supposed to help eliminate such problems, but often the limitless options available can actually hinder progress. Just look at the number of <a href="https://jamstack.org/generators/">static site generators</a> on which I <em>could</em> run this site.</p>

<p>The developer part of me probably wants to deal with every technicality; whereas what I should be doing is using what the technology affords me to produce the content!</p>

<h1 id="status">Status</h1>

<p>Currently, I'm still bouncing between the plain-text simplicity of Obsidian and the more structured, but seemingly less-portable, Standard Notes.</p>

<p>I've already <del>procrastinated</del> researched several hours trying to get <a href="11ty.dev/">11ty</a> and new versions of <a href="https://jekyllrb.com">Jekyll</a> working, and whilst I may eventually <em>like</em> to get back to that level of control over this blog, for now I'm sticking with a hosted blog platform - <a href="https://blot.im">Blot</a> for Obsidian-written posts, and <a href="https://listed.to">Listed</a> for Standard Notes ones, switching between the two to test and <em>optimise</em> my writing flow.</p>

<p><em>PS. This post was written on Obsidian :). I used the <a href="https://github.com/jdanielmourao/obsidian-sanctum">Sanctum</a> theme, and am publishing via git to Blot.</em></p>
]]>
      </description>
      <pubDate>Mon, 26 Dec 2022 12:36:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/indecision</link>
      <guid>https://www.chrisblunt.com/indecision</guid>
      <category>100days</category>
    </item>
    <item>
      <title>100 Days of Writing</title>
      <description>
        <![CDATA[<p>Whilst trying out <a href="https://standardnotes.com/">Standard Notes</a> and its <a href="https://listed.to">Listed</a> platform, I stumbled across the <a href="https://listed.to/@Listed/5202/100-day-writing-challenge">100 Day writing challenge</a>.</p>

<p>As I've been promising myself I'd write more on this site for years now - but consistently failing to do so - I thought this would be a good challenge to take up.</p>

<p>There seems to be a bit of a renaissance for traditional blogging, with platforms such as Listed, Hey World, write.as, and so on.</p>

<p>There'll likely be the odd day or two I miss posting, but hopefully I can find enough to fill up this blog with some thoughts, ideas, experiences, and the odd code tutorial (I'm spending a lot of time with cloud and serverless technologies at the moment).</p>

<p>Subscribe to get posts by <a href="%7B%7Bsite.email_subscribe_url%7D%7D">email</a> or <a href="https://www.chrisblunt.com/feed">RSS</a>.</p>
]]>
      </description>
      <pubDate>Sat, 24 Dec 2022 21:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/100-days-of-writing</link>
      <guid>https://www.chrisblunt.com/100-days-of-writing</guid>
      <category>100days</category>
    </item>
    <item>
      <title>Digital Clutter</title>
      <description>
        <![CDATA[<p>I'm going through the process of moving my notes from Evernote to .... somewhere. Obsidian? Standard Notes?  I'm not yet sure.</p>

<p>Going through this process has highlighted the amount of digital clutter I've archived over the years.</p>

<p>Many things aren't clutter - important documents and records, snippets and audio clips of first words, and various artwork and scribbles from over the years.</p>

<p>But do I also need to muddle them with bills and insurance documents from nearly two decades ago? Ticket confirmations from shows long-since seen?</p>

<p>As <a href="https://tonyedwardspz.co.uk/blog/the-web-is-wobbling/">Tony asks in his post</a>, I'm questioning the benefits of being encouraged to "save everything" over the years:</p>

<blockquote>
<p>Tools like Evernote trained us to capture everything. Scan a document. Send it to the cloud. Keep it forever. In all honesty, you’d have shredded the real thing by now.</p>
</blockquote>

<p>I remember that being one of the original features of GMail when I signed up ... No need to worry about storage, just throw everything into the archive...</p>

<p>It may have been a feature, but I'm not sure it's a benefit. It's more to think about; more to manage when you eventually want to move, or even hit the "delete" button.</p>

<p>If nothing else, knowing it is sitting there, <s>wasting</s> taking up storage, and forever building up can feel overwhelming. It's very much the digital equivalent of the messy desk. Multiplied by a seemingly infinite number of bytes.</p>

<p>Will I need that water bill from 2007? Probably not, but I'll leave it archived for now.</p>

<p><a href="https://www.theminimalists.com/jfw/">Just in case</a></p>
]]>
      </description>
      <pubDate>Fri, 23 Dec 2022 19:30:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/digital-clutter</link>
      <guid>https://www.chrisblunt.com/digital-clutter</guid>
      <category>100days</category>
    </item>
    <item>
      <title>Outdoors</title>
      <description>
        <![CDATA[<p><img src="https://assets.chrisblunt.com/2022/12/22/20221221-psp-trees-sky.jpg" alt="Tree in Plymouth Science Park with blue sky"></p>

<p>"Head out for a 20 or 30 minute walk" was my advice to <a href="https://www.plymouthsoftware.com">the PSW team</a> whenever they were stuck on a coding problem.</p>

<p>It is one of those lessons in life that I keep re-discovering for myself.</p>

<p>With the simple act of stepping outside, away from the keyboard, thoughts are clarified; solutions to problems suddenly appear; the mind has a chance to reset.</p>

<p>Although it's something that feels like it could be lost in the never-ending streams-of-notifications, time management tools, and productivity-"hacked" tasks that compete to fill our day.</p>

<p>There is something revitalising that comes with being outdoors.</p>

<p>Admittedly, the weather is not <em>always</em> ideal for getting out, but the act of stepping away from the keyboard, and giving your mind some space and time amongst nature, can pay dividends - and often help you fix that bug in your code.</p>
]]>
      </description>
      <pubDate>Thu, 22 Dec 2022 17:30:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/outdoors</link>
      <guid>https://www.chrisblunt.com/outdoors</guid>
      <category>100days</category>
    </item>
    <item>
      <title>Notes Apps and Data Portability</title>
      <description>
        <![CDATA[<p>One thing that's become increasingly important in the days of vendor lock-in and siloed data is the portability of that data. In my notes journey, this is something that's very much front-of-mind.</p>

<p>Whilst Evernote allowed exporting of data, its <code>.enex</code> files were a custom format, although there are plenty of importers available. I ended up using <a href="https://joplinapp.org/">Joplin</a>'s importer to convert my Evernote exports into folders of Markdown + Front Matter.</p>

<p>Markdown - which is just plain text - is ultimately portable. As long as plain text files are readable, then the notes I've captured over the years should be as well.</p>

<p>This blog has for years been powered by Markdown, starting with Jekyll, and moving to <a href="https://blot.im">Blot.im</a> and <a href="https://listed.to">Listed</a>.</p>

<h1 id="testing-for-portability">Testing for Portability</h1>

<p><a href="https://standardnotes.com/">Standard Notes</a> also uses markdown, so importing my notes was easy. However, as I mentioned yesterday, attachments were not imported, leaving me to manually upload and reconnect attachments with their notes.</p>

<p>Whilst gradually doing this across my notes, I decided to try exporting content from Standard Notes to ensure that my data was portable should I decide to move my notes in the future. (the note-taking apps space seems really busy at the moment!)</p>

<p>Unfortunately, whilst the Markdown data is exported, the attachments are not.</p>

<p>Most of the notes I've captured have been documents, receipt, artwork, or audio, making these exports redundant. I would be constrained to the way Standard Notes organises things (at least, without manual processing should I wish to move to another note platform).</p>

<h1 id="obsidian">Obsidian</h1>

<p>Alongside Standard Notes, I've also been trying out <a href="https://obsidian.md">Obsidian</a>, which - using a completely file-based system of organisation - allows attachments to notes to be preserved through simple Markdown <code>[title](./path/to/file)</code> links.</p>

<p>Thanks to the Joplin conversion of my Evernote files to Markdown+Front Matter, along with attachments in their own folder, getting my data into Obsidian was relatively easy.</p>

<p>Whilst this puts the responsibility of data management on the user, a purely file-based structure feels more future proof.</p>

<p>The ability to run scripts and tools across the files (e.g. normalising YAML front-matter, etc.) is an advantage.</p>

<p>Unlike Standard Notes, Obsidian doesn't include a blogging platform. However, being Markdown, the files can easily be published to a static provider, or a service such as <a href="https://blot.im">Blot</a>, again.</p>

<p>I'll continue working with Obsidian and Standard Notes to determine which of the two best fits my needs. However, the portability of data and ability to migrate to another service without too much manual processing is something that will be a key requirement.</p>
]]>
      </description>
      <pubDate>Wed, 21 Dec 2022 09:30:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/notes-portability</link>
      <guid>https://www.chrisblunt.com/notes-portability</guid>
      <category>100days</category>
      <category>apps</category>
      <category>blog</category>
      <category>note-taking</category>
    </item>
    <item>
      <title>On StandardNotes and Listed</title>
      <description>
        <![CDATA[<p><img src="https://assets.chrisblunt.com/2022/12/21/2022-12-21-screenshot-of-listed-blog.png" alt="Screenshot of blog on listed.to"></p>

<p>As part of my note-taking journey, I'm currently trying out <a href="https://standardnotes.com/">Standard Notes</a>.</p>

<p>Standard Notes seems to tick most boxes - multi-platform, reliable syncing, privacy-focussed and open-source, offline support, and it's <em>really</em> fast. I particularly like multiple editors for different types of content.</p>

<h1 id="attachments">Attachments</h1>

<p>The one thing that puts me off Standard Notes is its lack of support for embedded attachments, and the seemingly strange way it handles attachments altogether.</p>

<p>I used Evernote to capture everything - documents, artwork, audio recordings, as well as text-based notes. Unfortunately getting these into Standard Notes is a slow, manual process at the moment - and something I keep starting and giving up on.</p>

<p>Unfortunately, Standard Notes doesn't provide a way to import Evernote or Markdown files with attachments. Again, because of the way attachments are handled, this isn't something that can be done with the existing app.</p>

<p>As it's open source, though, I'm exploring if it's possible to add something to a custom build just to perform a one-time import.</p>

<p>Standard Notes has <a href="https://blog.standardnotes.com/40627/introducing-super-notes-moments-and-offline-file-access">recently added the Super Note</a> type which does allow for embedded content, and is a step in the right direction. The Super Note uses a custom JSON structure, though, so it strays away from the universal Markdown format.</p>

<p>Whilst it's possible to convert the JSON to Markdown, embedded content is lost. For data portability, I'd like to keep things in Markdown wherever possible.</p>

<h1 id="listed">Listed</h1>

<p>One of the things I really like about Standard Notes is the built-in blogging platform, <a href="https://listed.to">Listed</a>. By way of an experiment to see if it helps me to write more consistently, I tried switching <a href="https://www.chrisblunt.com/">this blog</a> over to Listed for a short time.</p>
]]>
      </description>
      <pubDate>Tue, 20 Dec 2022 09:30:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/on-standard-notes-and-listed</link>
      <guid>https://www.chrisblunt.com/on-standard-notes-and-listed</guid>
      <category>100days</category>
    </item>
    <item>
      <title>Moving to Blot</title>
      <description>
        <![CDATA[<p>This blog has existed for a while (since 2004!), and over the years I've used several different platforms to host and manage it. These have included custom builds, <a href="https://wordpress.org/">Wordpress</a>, <a href="https://en.wikipedia.org/wiki/Posterous">Posterous</a> (which was <a href="https://www.chrisblunt.com/moving-to-posterous">unfortunately cut short</a>), and - most recently - <a href="https://jekyllrb.com/">Jekyll</a>+Git+<a href="https://www.netlify.com/">Netlify</a>.</p>

<p>I much preferred static-site hosting, knowing that the site would never be locked into a platform as the content was stored simply as a set of Markdown text files. This meant that I could easily change things without needing to perform complicated export/imports across platforms.</p>

<p>As a ruby developer, I naturally settled on Jekyll, a popular static site engine that also powers <a href="https://docs.github.com/en/pages">Github Pages</a>.</p>

<p>However, every time I came to write something for the site, I found myself being drawn into the minutae of managing the code that built the site, tweaking configurations and templates, updating gem dependencies, and so on.</p>

<p>As a result of this <del>procrastination</del> development, I hardly ever got round to publishing anything!</p>

<h2 id="blot">Blot</h2>

<p>A couple of weeks ago, I stumbled across <a href="https://blot.im/">Blot</a>. Within a few hours of trying out the service, I'd decided to use it for this site.</p>

<p>Just like Jekyll and other static-site generators, Blot compiles a folder of markdown files into a website. Amongst other things, Blot also handles the hosting, CDN/image hosting, theming, integrations, and most other technicalities for you.</p>

<p>Just like Jekyll, the files can be hosted in a Git repository. However, for even easier management, a Dropbox or Google Drive folder can be used instead. You can learn more about Blot works <a href="https://blot.im/how">here</a>.</p>

<h2 id="balance">Balance</h2>

<p>For me, Blot seems to strike the right balance between a traditional CMS/hosted website (e.g, Wordpress), and the flexibility of a static-generated site.</p>

<p>This site is now running on Blot, and I'll likely be moving other sites here (such as <a href="https://www.plymouthsoftware.com/">plymouthsoftware.com</a> and <a href="https://valuepricing.uk/">valuepricing.uk</a>). I've also taken the opportunity to archive legacy posts and outdated tutorials to make some room for new content.</p>

<p>With Blot taking care of the technical detail, I hope to have far fewer distractions and so spend more time actually writing content and tutorials!</p>
]]>
      </description>
      <pubDate>Sat, 11 Dec 2021 13:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/moving-to-blot</link>
      <guid>https://www.chrisblunt.com/moving-to-blot</guid>
      <category>blog</category>
      <category>site</category>
    </item>
    <item>
      <title>1000 Days of Calm</title>
      <description>
        <![CDATA[<p><img src="https://assets.chrisblunt.com/2021/11/2021-11-30-calm-1000.jpg" alt="Calm 1000 Days on 29 November 2021"></p>

<p>I really enjoy using <a href="https://www.calm.com">Calm</a>, mostly for sleep stories. A few years ago, what started out as a couple of minutes each day eventually built to a streak of a few weeks.</p>

<p>This led to my developer-brain thinking "I wonder how it handles 4-digit numbers?". The sort of thing you find yourself wondering when writing software all day...</p>

<p>So started the work towards a goal of a 1000 daily meditations, "breathe" sessions, and/or sleep stories. Even if only a minute or two, it counted towards the goal.</p>

<p>Unfortunately, I broke the first streak at 535 days, and so had to start from scratch...Roughly two years and 9 months later, I finally got to 1000 days.</p>

<p>The next step, seeing how Calm deals with 5-digit numbers, is due in just over 24 years...</p>
]]>
      </description>
      <pubDate>Tue, 30 Nov 2021 13:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/calm-1000-days</link>
      <guid>https://www.chrisblunt.com/calm-1000-days</guid>
      <category>calm</category>
      <category>life</category>
      <category>meditation</category>
    </item>
    <item>
      <title>Rails and React: Fixing 'Module not found' errors for JSX files</title>
      <description>
        <![CDATA[<p>Recently, whilst exploring how we can use React with Rails at <a href="https://www.plymouthsoftware.com/">Plymouth Software</a>, we were coming up against an error trying to run the simple "Hello World" tutorial for the <a href="https://github.com/shakacode/react_on_rails">React on Rails</a> gem.</p>

<p>When compiling the assets through <code>bin/webpack-dev-server</code> or <code>bin/rails assets:precompile</code>, we were getting the following error message:</p>
<pre data-language="plain">Module not found: Error: Can't resolve '../bundles/HelloWorld/components/HelloWorld' in '/path/to/code/react-on-rails-app/app/javascript/packs'</pre>
<p>However, it turns out the message is a little misleading. I discovered this by accident by changing the import line:</p>
<pre data-language="diff"># hello-world-bundle.js
- import HelloWorld from '../bundles/HelloWorld/components/HelloWorld'
+ import HelloWorld from '../bundles/HelloWorld/components/HelloWorld.jsx'</pre>
<p>Trying to compile the assets again gave a different error:</p>
<pre data-language="plain">HelloWorld.jsx: Support for the experimental syntax 'jsx' isn't currently enabled (8:5)</pre>
<p>The error seems to be that the <a href="https://reactjs.org/docs/introducing-jsx.html">JSX</a> (the HTML-in-Javascript template language used by React) could not be compiled.</p>

<p>But JSX should come as part of the React installation, so I tried re-running the webpacker react generator:</p>
<pre data-language="sh">$ bin/rails webpacker:install:react</pre>
<p>From the logs, I could see this added <code>babel</code> (amongst other things) to the <code>package.json</code>.</p>

<p>After restarting <code>bin/webpack-dev-server</code> and <code>bin/rails s</code>, the tutorial page worked as expected!</p>

<p><img src="https://assets.chrisblunt.com/2020/07/react-on-rails-hello-world_gcjh5w.png" alt="React on Rails Hello World Screenshot"></p>
]]>
      </description>
      <pubDate>Wed, 22 Jul 2020 15:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-and-react-fixing-module-not-found-errors-for-jsx-files</link>
      <guid>https://www.chrisblunt.com/rails-and-react-fixing-module-not-found-errors-for-jsx-files</guid>
      <category>fix</category>
      <category>howto</category>
      <category>jsx</category>
      <category>rails</category>
      <category>react</category>
      <category>react-on-rails</category>
    </item>
    <item>
      <title>Using MariaDB / MySQL Service Containers Azure in Devops Pipeline</title>
      <description>
        <![CDATA[<p>I've recently been working a Rails application CI to <a href="https://dev.azure.com">Azure Devops Pipelines</a> feature. As part of the migration, the app is also being bundled into Docker containers (for CI to get started).</p>

<p>One of Pipelines' features is allowing the setup of Docker containers to provide services, such as databases, to a job whilst running the job itself inside a container.</p>



<p>Unfortunately, when I configured a service container to run a MariaDB instance, the pipeline would try to run the jobs before the database had initialised:</p>
<pre data-language="bash">Waiting for MySQL database to accept connectionsERROR 2002 (HY000): Can't connect to MySQL server on 'db' (115)
.ERROR 2002 (HY000): Can't connect to MySQL server on 'db' (115)
.ERROR 2002 (HY000): Can't connect to MySQL server on 'db' (115)
...</pre>
<p>Service Containers in pipelines <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/service-containers?view=azure-devops&amp;tabs=yaml#healthcheck">do make use of the image's <code>HEALTHCHECK</code></a>command, but at the time of writing, the MariaDB images don't appear to include a healthcheck; and the Pipelines config file doesn't support adding one at runtime, in the same way that you might in a <code>docker-compose.yml</code> file.</p>

<p>As a workaround, I tried <code>sleep</code>ing the tests to wait for the database to initialise, but regardless of the sleep time this didn't seem to work. I also wasn't happy including a hard-coded "wait" time which, even if it had worked, might lead to randomly-failing job runs depending on conditions.</p>

<p>Eventually, I found a working solution by using a short step that repeatedly attempts to connect to the the database server.</p>

<p>To set this up for your own project, first, make sure the Docker image you are using for your tests installs the <code>mysql</code> command, e.g.:</p>
<pre data-language="dockerfile"># Dockerfile (repository/yourappimage)
FROM ruby 2.6

RUN apt-get update &amp;&amp; apt-get install -y mariadb-client

# ...

CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]</pre>
<p>Next, in your <code>azure-pipelines.yml</code> file, configure the service and job containers:</p>
<pre data-language="yaml"># azure-pipelines.yml
trigger:
- master

resources:
  containers:
  - container: db
    image: mariadb:10.1
    env:
      MYSQL_DATABASE: my_app_test
      MYSQL_ROOT_PASSWORD: superSecret123

jobs:
- job: specs
  displayName: Run the specs

  pool:
    vmImage: 'ubuntu-latest'

  services:
    db: db

  container:
    image: 'repository/yourappimage'
    endpoint: 'your-docker-hub-or-acr-service-connection'
    env:
      RAILS_ENV: test
      RACK_ENV: test
      DATABASE_URL: mysql2://root:superSecret123@db:3306/my_app_test

  steps:
  - script: |
      printf 'Waiting for MySQL database to accept connections'
      until mysql --host db --user=root --password=superSecret123 --execute "SHOW DATABASES"; do
        printf '.'
        sleep 1;
      done;
    displayName: Wait for database to initialise

  - script: bundle exec rails db:create db:migrate
    displayName: Prepare test db

  - script: bundle exec rails test
    displayName: Run specs</pre>
<p>The step that checks for connectivity to the database server is at at line 32. It attempts to run a simple <code>SHOW DATABASES</code> command from our job container.</p>

<p>If that command fails, it <code>sleep</code>s for 1 second, and then tries again, repeating this process until the connection is successful (or the job times out).</p>

<p>This prevents the next step - creating the test database schema - from running until our app's container has confirmed that it can connect to the database server.</p>

<p>I hope this is useful for anyone using Azure Devops Pipelines as part of their CI setup. It may also be applicable to other CI services, including the new Github Actions, which allow the use of service containers to provide services to your tests.</p>
]]>
      </description>
      <pubDate>Sun, 22 Dec 2019 09:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/using-mariadb-mysql-service-containers-azure-in-devops-pipeline</link>
      <guid>https://www.chrisblunt.com/using-mariadb-mysql-service-containers-azure-in-devops-pipeline</guid>
      <category>azure</category>
      <category>devops</category>
      <category>howto</category>
      <category>mariadb</category>
      <category>mysql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Business of Software at FutureSync 2019</title>
      <description>
        <![CDATA[<p>Last week I was lucky enough to give a talk at the FutureSync 2019 conference in Plymouth. Entitled "The Business of Software", taking the time to prepare and practice the talk gave me a chance to reflect on running my business for coming up to 10 years.</p>

<p>This post summarises some lessons I've learned on my journey so far.</p>



<p>Last week, <a href="https://www.plymouthsoftware.com/">Plymouth Software</a> proudly sponsored the <a href="https://futuresync.co.uk">FutureSync</a> conference held at Plymouth University.</p>

<p>It was fantastic to see how much the conference has grown in just a couple of years. The event was a huge success, and I was also lucky enough to have the opportunity to give another talk. The topic I chose was somewhat-loosely titled <em>“The Business of Software: Lessons Learned So Far”.</em></p>

<div class="videowrap">
  
</div>

<p>Taking some time away from the day-to-day running of business is a crucial - but also incredibly difficult to do when there is so much else to be done “in the business”!</p>

<p>In writing and practicing the talk, I had a lot of time to reflect on my experiences so far running Plymouth Software, and take some time to think about the bigger strategy moving the business forward.</p>

<p>Here I've summarised some of the key topics from the talk.</p>

<h2 id="start-up-days">Start-up Days</h2>

<p>When I first started the business, I made a point of <a href="https://www.chrisblunt.com/business-update-october-2010/">writing about my experiences</a>. I really enjoyed the process of writing about my business highs and lows “in the open”. I was inspired by people such as <a href="https://nathanbarry.com/">Nathan Barry</a> and the team at <a href="https://basecamp.com/">Basecamp</a> (then 37signals), and countless others.</p>

<p>However, as time when on two things happened that reduced the frequency of (and eventually stopped!) these posts :</p>

<ol>
<li>As things got busier, I didn't devote as much time to blogging (and marketing in general - a big mistake); and</li>
<li>The more I was learning about the running business, the more I began to feel the effects of <a href="https://en.wikipedia.org/wiki/Impostor_syndrome">imposter syndrome</a>.</li>
</ol>

<p>Both of these are still the case - business is running well at the moment, and I have some ambitious growth plans for 2019-20; and to some degree I still spend a <em>lot</em> of time wondering if I'm doing things “the right way” to build my business (whatever that may be!)</p>

<h2 id="looking-back">Looking Back</h2>

<p>In 2020, Plymouth Software will turn 10 years old. That alone is worrying, as it really does feel like I'm just getting started.</p>

<p>But as I came to terms with this whilst writing my talk, it seemed like a good opportunity to reflect on some of the lessons I've learned, and things I wish I'd have known when starting out.</p>

<p>It turns out there are so many things I could have spoken about. There were so many things I feel I've learned (but not necessarily taken action on!) that it was impossible to squeeze them all into a 20-minute session.</p>

<p>So instead, I focussed on four of the “headline” lessons I felt summed up where I've been, and where I want to be.</p>

<h2 id="build-your-audience">Build Your Audience</h2>

<p>Quite simply, a business should have the biggest audience possible.</p>

<p>Most advice out there is to niche, and that is important, but I think - especially when starting out - you just need to gain exposure for your business.</p>

<p>With the wealth of networks (both on- and off-line) that are available nowadays, audiences will naturally build around the content you are delivering.</p>

<p>Whilst I've spent many years trying to remain “small” and focussed, I'm now trying to grow the businesses audience as much as possible.</p>

<blockquote>
<p>"Obscurity is your Enemy" - <a href="https://medium.com/r/?url=https%3A%2F%2Fgrantcardone.com%2F">Grant Cardone</a>, The 10X Rule</p>
</blockquote>

<p>As soon as I heard this quote by Grant Cardone, it resonated immediately. I realised that was describing how I'd been running my business - remaining obscure and trying to fly “under the radar”, rather than thinking about growing awareness of the brand and building an audience.</p>

<p>Rather than worrying about niching, just try to attract as many people to your brand and content as possible. You will naturally niche your audience by attracting people with whom your content resonates.</p>

<p>One of the most important things I missed for several years was to build an email list. A well-managed, quality email list gives you an audience of people who are interested in, and have given you their permission, to talk about your business, products, and services.</p>

<p>In the early days, I started with an email newsletter using <a href="https://mailchimp.com/">Mailchimp</a>. But, after sending a handful of newsletters, the “news” dried up!</p>

<p>Nowadays, I build knowledge-based email courses using <a href="https://convertkit.com">ConvertKit</a>. Building courses and sequences in this way provides genuine value to new and existing members of the list.</p>

<p>The primary course I offer is the free <a href="https://healthyrailsapps.com">Keep Your Rails Apps Healthy Course</a>, which gives practical guidance on how to secure, monitor, maintain and optimise Ruby on Rails applications (You can join for free at <a href="https://healthyrailsapps.com">https://healthyrailsapps.com</a>).</p>

<p>Whichever tool you use (ConvertKit, Drip, Mailchimp, etc.), the important thing is to build in valuable content and set up automated sequences to help automate a big section of your marketing.</p>

<h2 id="understand-value">Understand Value</h2>

<p>I talk a lot about <a href="https://valuepricing.uk/">value pricing</a>, but pricing is only a small part of the story. The bigger goal is to shift your mindset away from providing a service towards providing a <em>solution</em>.</p>

<p>Yes, providing the highest quality of service is critical - but only as a means of delivering solutions to your clients' business problems.</p>

<p>I spent years talking about the tools and technologies, processes and methodologies I'd be using to work with clients. It's only natural, as developers, that we focus on the code (and editors, frameworks, servers, and so on…)</p>

<p>But that is generally not relevant to a client. There are exceptions, but a client who comes to you with a business problem (as opposed to a client who is simply looking to outsource an employee's role) is interested in solving that problem using the most effective, efficient methods possible.</p>

<p>It's taken me a long time to come to terms with this, but good clients shouldn't be as interested in how well-formed your code is (or worse - how many hours you'll be at your desk!) as much as they are by how effectively you can solve their business problems[^1].</p>

<p>Ultimately, the client is focussed on their problem, and how you can solve it.</p>

<p><a href="https://jonathanstark.com/">Jonathan Stark</a> talks about trying to talk new clients <em>out</em> of working with you. This is a great way to ensure your sales process is truly about the value you can deliver, and that the client has a real interest in engaging your services.</p>

<p>If your potential client is able to exhaust all your reasons for not working with them, you have a pretty strong case that you can deliver a solution, and that they will value your work.</p>

<h2 id="productise">Productise</h2>

<p>Bringing together the previous two lessons - building an audience, focussing on business problems, and ultimately extracting common needs, leads to productisation.</p>

<p>Productising services is really the key to shifting away from a service provider model to a solution provider.</p>

<p>The goal is to identify common needs and requirements amongst your audience and customer base, and begin to extract common activities that deliver results.</p>

<p>These activities can be documented; in fact, the more you can put into step-by-step instructions, the better. Whilst there are apps out there that will help you build these standard operating procedures (SOPs), I find that Google Docs or Evernote works just fine.</p>

<p>As you build more and more systems and processes in your business, you can document, repeat, delegate and ultimately scale.</p>

<p>For in-depth resources on this topic, and great information and courses on how to productise your services, check out <a href="https://briancasel.com">Brian Casel's</a> work, and specifically the <a href="https://productizecourse.com/">Productize</a> course.</p>

<h2 id="business-mindset">Business Mindset</h2>

<p>This was the big lesson, that feels like it sums up my current thinking.</p>

<p>It's the inevitable result of the other three, especially productisation. The mindset shift from employee/freelancer/contractor/consultant to business owner is huge, and probably one of the most difficult things to do when you're running your own business.</p>

<p>As a solo consultant, you wear all the hats in your business, from big-strategy direction through sales, project management, and operations to payroll, bookkeeping and ordering the coffee (one of the most critical roles).</p>

<p>The biggest lesson, then, is perhaps to let go of certain tasks in your business. Accept that you cannot do everything yourself, and look for ways to delegate the repeatable parts of your business.</p>

<p>As more of your business becomes repeatable - more systematised - you should be able to start focussing more on the more valuable parts of your business. Delivering on the stuff that gives the greatest, most beneficial return to your clients, and thinking more about the bigger strategy rather than worrying about the minutiae.</p>

<h2 id="today">Today</h2>

<p>That pretty much sums up where I feel I am now. It's taken me a lot longer than it should to realise, but I feel the time is right to grow the business, by starting to <a href="https://www.plymouthsoftware.com/careers">build a team</a>, focussing on our core services, constantly improving what we offer to clients, and growing the overall number of clients we serve.</p>

<p>Running your own business gives you incredible freedom and flexibility, but also brings with it huge challenges, worries, highs and lows.</p>

<p>It seems strange to say after so many years, but it really does feel like I'm just getting started. Whilst I've learned so much, I know that there is so much more and there are new ideas, opportunities and lessons in the years ahead.</p>

<p><em>I can't wait to get started.</em></p>

<p>[^1]:   Caveat: Obviously, your code should be well-structured, architected, fully tested and compliant with best practices, but that  should be implicit as part of you providing a professional, high-quality service to clients.</p>
]]>
      </description>
      <pubDate>Fri, 03 May 2019 10:50:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/business-of-software-future-sync-2019</link>
      <guid>https://www.chrisblunt.com/business-of-software-future-sync-2019</guid>
      <category>business</category>
      <category>conference</category>
      <category>futuresync</category>
      <category>plymouthsoftware</category>
    </item>
    <item>
      <title>2018: A Value Pricing Year in Review</title>
      <description>
        <![CDATA[<p><em>It seems like only a few months ago I decided to make the switch to value pricing, yet - to my amazement / surprise / concern / shock / horror! - here we are nearly 3 years on…</em></p>

<p>The past 12 months have been quite an experience in my business journey. I was lucky enough to work on some great new projects, and continued my drive towards a more focussed, product-orientated business.</p>

<p>But I also found myself having moments of uncertainty with value pricing.</p>

<p>This post is a summary of what I feel went right and wrong, and how I plan to update my value pricing strategy in 2019.</p>

<h1 id="productising-services">Productising Services</h1>

<p>This year I made a big effort to standardise my services, replacing where I could bespoke offers with products and packages. The goal of productising a service is to make the work - or at least the process - as repeatable as possible.</p>

<p>I think productising can work hand-in-hand with value pricing as transition your offer to be more about that value of the service and outcomes you deliver.</p>

<p>A productised service should be more easy to scale and increase its value to both parties. As time goes on, prices can be adjusted to better reflect value, and optimise the service to increase efficiency (and the value to you)</p>

<p>That being said, one conflict between Productised services and Value Pricing is that by publishing a price upfront, you are not charging for the true value.
This, then, may not be truly value pricing (or perhaps it is not true productising), but it has certainly felt like a good stepping stone on the journey.</p>

<p>As <a href="https://medium.com/u/966a6de1b3ea">Jonathan</a> mentions in his article, I've ended up using productised services as a part of a product ladder, with fixed-price smaller engagements, and bespoke-priced larger engagements such as <a href="https://www.plymouthsoftware.com/rails-service-plans/">service plans</a> and <a href="https://www.plymouthsoftware.com/ruby-on-rails-upgrade-service/">Rails application upgrades</a> (see below).</p>

<h1 id="code-reviews">Code Reviews</h1>

<p><a href="https://www.plymouthsoftware.com/code-review/">Code Reviews</a> were perhaps my most frequent example of a productised service, as I found they became a pre-requisite to any higher-value service that I offered.</p>

<p>Code Reviews were easy to define and limit in their scope. They had clear outcomes, and an easy to convey value for customers. The report customers received as a result of the review gave them a snapshot of their current situation, and recommendations for the next best steps. Code Reviews acted as a great first engagement, with low risk for all parties.</p>

<p>For me, the Code Reviews were a great way to gain a deeper understanding of an application's code and infrastructure, and better appreciate what might be required to achieve the customer's goals.</p>

<p>I'll continue to offer stand-alone Code Reviews and incorporate them into other services.</p>

<h1 id="consultations">Consultations</h1>

<p>I recognised that sometimes a full-blown code review (which can take a few days to produce, depending on the complexity of the underlying code) may be too much at the very early stages of an enquiry.</p>

<p>For smaller engagements, such as those initial conversations that establish the value of a project, I offered a consultation service.</p>

<p>I've found the consultations haven't had much uptake though. Most incoming projects have progressed from a quick 15-minute phone call to the Code Review itself.</p>

<p>The <a href="https://www.plymouthsoftware.com/codeshot-consultation/">Consultation</a> offer is a little too generic at the moment, with little definition about who they are aimed at. I plan to change this in the coming weeks.</p>

<h1 id="codeshot-ruby-on-rails">CodeShot: Ruby on Rails</h1>

<p>I offered <a href="https://www.plymouthsoftware.com/ruby-on-rails-micro-consultation/">CodeShots</a> as a "one-hit" piece of development work. Ideally a small, discrete piece of work that can be turned around quickly.</p>

<p>Whilst I have carried out a handful of these with some success, I've found it to be very difficult to isolate development projects to such a degree.</p>

<p>Based on what I've appreciated about value pricing this year, and recognising that such small jobs have little value beyond "needing to be done", I plan to retire (or at least redesign) the CodeShot offer.</p>

<h1 id="rails-upgrades">Rails Upgrades</h1>

<p>Rails Upgrades have been a staple product this year, and seem popular as Rails and applications built on it mature. That being said, some of those upgrades I've carried out have presented <em>huge</em> challenges.</p>

<p>This has been difficult to recognise (and admit to myself!), but where I thought I was value pricing upgrades as a service, looking back I can see that I was really pricing them as fixed-priced projects. I really had little understanding of the value that such upgrades deliver.</p>

<p>As Rails Upgrades can hide so many unknowns (although I've learned a few new red flags to look out for this year!), I plan to change how they are offered in the new year.</p>

<p><em>Sidenote: <a href="https://githubengineering.com/upgrading-github-from-rails-3-2-to-5-2/">GitHub published a great article</a> on how they upgraded from Rails 3.2 to 5, with some important lessons they learned during the process.</em></p>

<h1 id="service-plans">Service Plans</h1>

<p>One of my earliest attempts at productisation was to introduce service plans. These are essentially retainer agreements that are limited by scope rather than time.</p>

<p>The scope of each service agreement is determined in conversation with the customer, based on their longer-term goals.</p>

<p>However, the service plans all feature some common benefits, such as ongoing upgrades and bug-fixes, monitoring, etc.</p>

<p>In the early days I made some major pricing mistakes with service plans. I've also learned it's fairly difficult to scope retainer agreements where there is a lot of ambiguity about the end project.</p>

<p>In line with the rest of the business, I've also spent the past year or so constraining service plans to Rails applications (rather than including mobile apps as was the case).</p>

<p>In 2018, a number of service plans came to their natural end. Whilst this may be beneficial from a long term business strategy perspective, there's no denying that losing that regular monthly income was difficult.</p>

<p>I still plan to offer service plans as a core part of the business, although will continue to revise what is offered, and spend more time assessing the value for each plan.</p>

<p>To that end, it may be that Service Plans have an annual review, or are offered as a foundation for other services. I also feel that more vertical specialisation may be required, knowing that this would affect the business as a whole.</p>

<h1 id="moments-of-uncertainty">Moments of Uncertainty</h1>

<p>The previous 12 months have forced me to think about my approach to value pricing, as doubt has started to creep into my mind due to a number of value pricing mistakes I've made.</p>

<h1 id="rediscovering-value-pricing-benefits">Rediscovering Value Pricing Benefits</h1>

<p>Whenever I've had concerns or doubts about using value pricing, I usually read great articles for inspiration and listen to podcasts and find myself re-discovering its benefits.</p>

<p>I understand that the mistakes are mine and due to my misinterpreting or incorrectly applying value pricing principles. Whilst when I started this journey, I was fairly insistent on applying value pricing to everything.</p>

<p><a href="https://valuepricing.uk/value-pricing-mistakes-and-what-ive-learned-from-them-7ee27a8c1083/">I've had to accept there are situations where it is not always best-suited.</a></p>

<h1 id="when-in-doubt">When In Doubt…</h1>

<p>Agency-style work is one such example that I now avoid value pricing. This year I've worked on a couple of agency-style projects.</p>

<p>These projects have been genuinely interesting, and rewarding project. I get to work with great teams on much larger projects than I would normally.</p>

<p>However, it's clear that there is nothing like going back to filling in time sheets and concerning yourself with inputs over outcomes to remind you of the benefits of value pricing!</p>

<h1 id="onwards">Onwards</h1>

<p>I always intended this experiment to be a journey. Admittedly at the start, I hadn't wanted to accept that there might be areas where value pricing wasn't appropriate - especially as I felt so strongly that time-based billing is bad for everyone.</p>

<p>Three years on, though, I've come to accept that such circumstances can and do occur.</p>

<p>Rather than stubbornly trying to fit everything into a value pricing, I think it may be best to accept these, and instead concentrate on adjusting the business itself such that value pricing is the default.</p>

<p>In 2019, I plan to further tighten up the services that are offered, specialise more, concentrate on products (and productisation).</p>

<hr>

<p>How was 2018 for your business? Did you try value pricing? Please comment below or <a href="https://www.chrisblunt.com/contact/">get in touch</a> .</p>
]]>
      </description>
      <pubDate>Sat, 05 Jan 2019 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/2018-a-value-pricing-year-in-review</link>
      <guid>https://www.chrisblunt.com/2018-a-value-pricing-year-in-review</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Rails 5: Matching Asset URLs in Feature Specs</title>
      <description>
        <![CDATA[<p>As part of an ongoing Rails Upgrade for a large web application, I needed to update a feature spec that checked the URL of included assets. In earlier versions of Rails, these URLs could simply be checked by calling:</p>
<pre data-language="ruby">expect(find('img.my_asset_image')[:src]).to end_with('/my-image.png')</pre>
<p>This expectation would pass given the following ERB output:</p>
<pre data-language="ruby"># &lt;%= image_url('my-image.png') %&gt;
# =&gt; &lt;img src="/assets/my-image.png" /&gt;</pre>
<p>In Rails 5, though, assets are compiled and fingerprinted during the test runs. This resulted in the generated URL containing a random fingerprint that could not be determined until runtime:</p>
<pre data-language="ruby"># &lt;%= image_url('my-image.png') %&gt;
# =&gt; &lt;img src="/assets/my-image-aa00bb11cc22dd33ee44ff55.png" /&gt;</pre>
<h2 id="writing-a-simple-custom-matcher">Writing a Simple Custom Matcher</h2>

<p>To resolve this issue, I ended up writing a small custom matcher for RSpec that matched the URL against a simple regular expression.</p>


  I also tried using <code>asset_url</code> to determine the true asset URL, but with no luck. The generated URL didn't contain a fingerprint.

<pre data-language="ruby"># spec/support/custom_matchers.rb
RSpec::Matchers.define :eql_asset_url do |asset_name|
  match do |actual|
    pattern = asset_matcher_pattern(asset_name)
    expect(actual).to match(pattern)
  end

  failure_message do |actual|
    pattern = asset_matcher_pattern(asset_name)
    "expected #{actual} to contain an image asset url matching #{pattern}"
  end

  def asset_matcher_pattern(asset_name)
    # Split the asset's path, filename and extension for matching
    pathname = Pathname.new(asset_name)
    dirname = pathname.dirname.to_s
    dirname = '' if dirname == '.'
    extname = pathname.extname.to_s
    basename = pathname.basename.to_s.gsub(extname, '')

    %r{/assets#{dirname}/#{basename}-[a-f0-9]*#{extname}}
  end
end</pre>
<p>Now in my feature specs, I can use the <code>eql_asset_url</code> matcher to test generated asset URLs:</p>
<pre data-language="ruby">expect(find('img.my_asset_image')[:src]).to eql_asset_url('my-image.png')</pre>
]]>
      </description>
      <pubDate>Fri, 05 Oct 2018 10:50:32 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-5-matching-asset-urls-in-feature-specs</link>
      <guid>https://www.chrisblunt.com/rails-5-matching-asset-urls-in-feature-specs</guid>
      <category>rails</category>
      <category>rspec</category>
      <category>ruby</category>
      <category>specs</category>
      <category>testing</category>
    </item>
    <item>
      <title>Rails on Docker: How to Share Containers Across Multiple Projects</title>
      <description>
        <![CDATA[<p>When running your applications in containers, you usually want to isolate everything so that the app can be independent of any external . There are some cases, though, where it might be desirable to share a single container across multiple projects.</p>

<p>For example, when simulating a micro-services architecture in development, you might want to share a single database container across two or more applications, so that they can both access the same data.</p>

<p>Docker and Docker Compose make this possible through the use of Docker networks, allowing containers from different compose projects to be attached to the same network.</p>

<h2 id="sharing-containers-in-compose-files">Sharing Containers in Compose Files</h2>

<p>Suppose we have two entirely separate Rails applications that need to connect to a single database container. We can achieve this by using two docker-compose files, and specifying a single network to which our containers will be connected.</p>

<p><strong>→ Create a configuration file, <code>docker-compose.1.yml</code>:</strong></p>
<pre data-language="yaml"># docker-compose.1.yml
version: '3.2'

services:
  db:
    image: postgres
    environment:
      - POSTGRES_DB=my_app_development
      - POSTGRES_PASSWORD=password

  web_one:
    image: cblunt/rails-basic-app
    depends_on:
      - db
    ports:
      - '3000:3000'
    environment:
      - RAILS_ENV=development
      - DB_HOST=db
      - DB_USER=postgres
      - DB_PASSWORD=password
      - DB_NAME=my_app_development
    command: /bin/sh -c 'bin/rails db:migrate &amp;&amp; bin/rails s -b 0.0.0.0 -P 3000'</pre>
<p><strong>→ Next, create a second configuration file, <code>docker-compose.2.yml</code>, in the same folder.</strong></p>

<p>This configuration file will declare our 'second' web application (in this case, just a second instance of the same application).</p>

<p>Note that we only need to declare the web application itself in this configuration. We'll also expose <code>web_two</code> on port 3001, to prevent port clashes on our network:</p>
<pre data-language="yaml"># docker-compose.2.yml
version: '3.2'

services:
  web_two:
    image: cblunt/rails-basic-app
    ports:
      - '3001:3001'
    environment:
      - RAILS_ENV=development
      - DB_HOST=db
      - DB_USER=postgres
      - DB_PASSWORD=password
      - DB_NAME=my_app_development
    command: /bin/sh -c 'bin/rails s -b 0.0.0.0 -p 3001'</pre>
<p>Now run <code>docker-compose up</code> on the two files. As they are in the same directory, Docker Compose will create a single default network, and attach all the containers to that network:</p>
<pre data-language="shell">$ docker-compose -f docker-compose.1.yml -f docker-compose.2.yml up -d

Creating network "project_default" with the default driver
Creating project_db_1 ... done
Creating project_web_two_1 ... done
Creating project_web_one_1 ... done</pre>
<p>In your browser, open the first application (running on port 3000). If you've used my example image above, open <a href="http://localhost:3000/posts">http://localhost:3000/posts</a>. Use the simple scaffold to create a new blog post entry:</p>

<p><img src="https://assets.chrisblunt.com/2018/10/2018-10-03-add-post-app-one.gif" alt="Add a Post to App One"></p>

<p>Once you have added a post, visit the second instance of the application (running on port 3001). You will see that the post you added shows up here as well, so both our applications are talking to the same database:</p>

<p><img src="https://assets.chrisblunt.com/2018/10/2018-10-03-add-post-app-two.gif" alt="Add a post to App Two"></p>

<h2 id="sharing-containers-across-different-projects">Sharing Containers Across Different Projects</h2>

<p>So far, everything has worked pretty much as expected when your compose files are in the same project folder, but what if you have two compose files in entirely separate projects that need to share a common container?</p>

<p>For example, lets assume you have a project with the following folder structure:</p>
<pre data-language="plain">+ project_one/
|  - docker-compose.yml
|
+ project_two/
|  - docker-compose.yml</pre>
<p>As before, we can use Compose's ability to load multiple configuration files to bundle everything into one network:</p>
<pre data-language="shell">$ docker-compose -f project_one/docker-compose.yml -f project_two/docker-compose.yml up -d</pre>
<p>Again, Compose will create a single default network, and attach all the containers to that network.</p>

<p>Although this works as before, if dealing with multiple projects, I'd prefer to explicitly declare to which network the containers are attached. This helps to prevent any confusion when debugging our configuration.</p>

<p>You can declare a default network using the <code>networks</code> entry in your <code>docker-compose.yml</code> files:</p>
<pre data-language="yaml"># project_one/docker-compose.yml
version: '3.2'

networks:
  default:
    external:
      name: my_net

services:
  # ...</pre><pre data-language="yaml"># project_two/docker-compose.yml
version: '3.2'

networks:
  default:
    external:
      name: my_net

services:
  # ...</pre>
<p>Note that when declaring an external network manually, Compose will <em>not</em> automatically create it. Therefore, we need to create the network manually using <code>docker network</code> first:</p>
<pre data-language="shell">$ docker network create my_net</pre>
<p>Now we can use the compose configurations independently, knowing that any containers declared within them will be attached to the same network:</p>
<pre data-language="shell">$ docker-compose -f project_one/docker-compose.yml up -d
$ docker-compose -f project_two/docker-compose.yml up -d</pre>
<p>As before, both <code>web</code> containers will have access the same database.</p>

<h2 id="next-steps">Next Steps</h2>

<p>Docker's networking allows you to build complex micro-service architectures, and share container resources with multiple applications. Docker Compose is great for simulating this type of set up in development.</p>

<p>Once you start running your apps in production, you can take advantage of more powerful orchestration tools, such as <a href="https://www.plymouthsoftware.com/articles/rails-on-docker-run-your-rails-app-on-docker-swarm/">Docker Swarm</a>, to configure and manage your applications and their environment.</p>
]]>
      </description>
      <pubDate>Wed, 03 Oct 2018 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-on-docker-share-containers-across-multiple-projects</link>
      <guid>https://www.chrisblunt.com/rails-on-docker-share-containers-across-multiple-projects</guid>
      <category>compose</category>
      <category>docker</category>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Rails on Docker: Using an Entrypoint File in your Containers</title>
      <description>
        <![CDATA[<p>When using Docker to run your Rails apps in development, you might come across a leftover <code>server.pid</code> file in your app's <code>tmp/</code> folder.</p>

<p>This file is created when you run <code>rails server</code> to start your app. If your container is not shut down properly, this <code>server.pid</code> file is not removed. This prevents you from starting a new container; as the file is copied into the new container, you will be confronted with the following message when you try to run your app:</p>
<pre data-language="ruby">web_1    | A server is already running. Check /usr/src/app/tmp/pids/server.pid.
...
web_1    | Exiting</pre>
<p>Of course, the simple way to resolve this is to remove the offending file (<code>$ rm tmp/pids/server.pid</code>) and restart your container.</p>

<p>This can quickly become annoying, though, and won't help if (for example) you are trying to scale the number of containers in your development environment. Every new container will inherit the <code>tmp/pids/server.pid</code> file, and so exit immediately.</p>

<h2 id="using-an-entrypoint-file">Using an Entrypoint File</h2>

<p>To properly handle this situation is to use a docker entrypoint file. This is a simple shell script that becomes the command that is called when your container starts.</p>

<p>Entrypoint files are commonly used to set up or configure a container at runtime. For example, when you run the <code>postgres</code> or <code>mariadb</code> containers, their entrypoint files create and configure a default database.</p>

<p>An example entrypoint file for our Rails app to resolve the problem above might be:</p>
<pre data-language="shell">#!/bin/sh

rm -f tmp/pids/server.pid
bin/rails server -b 0.0.0.0 -p $PORT</pre>
<p>Save this file as <code>docker-entrypoint.sh</code> in your app's folder (in the same location as <code>Dockerfile</code>). You'll also need to make sure it is executable by modifying its permissions:</p>
<pre data-language="shell">$ chmod u+x ./docker-entrypoint.sh</pre>
<p>Next, in your app's <code>Dockerfile</code>, set the <code>CMD</code> line to run your new entrypoint file:</p>
<pre data-language="dockerfile"># Dockerfile
FROM ruby:2.5
# ...
EXPOSE $PORT
CMD ./docker-entrypoint.sh</pre>
<p><em>Note that you could use the</em> <code>_ENTRYPOINT_</code> <em>command in this instance, but as we're supplying default arguments (</em><code>_server_</code><em>,</em> <code>_-b_</code> <em>and</em> <code>_-p_</code><em>) to the</em> <code>_bin/rails_</code> <em>command, I prefer to use</em> <code>_CMD_</code> <em>in line with the</em> <a href="https://docs.docker.com/engine/reference/builder/#cmd"><em>Docker documentation</em></a><em>.</em></p>

<p>Alternatively, if you only want to use your entrypoint file for local development, you could simply override the container's default command in your <code>docker-compose.yml</code> file:</p>
<pre data-language="yaml"># docker-compose.yml
# ...
web:
  build: .
  command: ./docker-entrypoint.sh
  # ...</pre>
<p>Now you can spin up your container(s) in the normal way using Docker compose:</p>
<pre data-language="shell">$ docker-compose up -d --scale web=2</pre>
<h2 id="next-steps">Next Steps</h2>

<p>In this simple example, you've seen how to use an entrypoint file to initialise the state of a container at runtime. You can also use entrypoint files to perform much more complicated setup, such as creating runtime files and databases, configuring local services, and more.</p>
]]>
      </description>
      <pubDate>Thu, 23 Aug 2018 10:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-on-docker-using-an-entrypoint-file-containers</link>
      <guid>https://www.chrisblunt.com/rails-on-docker-using-an-entrypoint-file-containers</guid>
      <category>docker</category>
      <category>entrypoint</category>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Business Tools: How to get started with ConvertKit for Email Automation</title>
      <description>
        <![CDATA[<p><em>Welcome to this new series where I'll dive into the tools and services I use and have used in my business.</em></p>

<p><em>In this first post, I'll be looking at</em>  <a href="https://convertkit.com" title="ConvertKit"><em>ConvertKit</em></a><em>, which I use for email marketing automation.</em></p>

<p><img src="https://assets.chrisblunt.com/2018/08/convertkit-automations.jpg" alt=""></p>

<p>When I started <a href="https://www.plymouthsoftware.com">my business</a> (which is a worrying 8 years ago now!), my knowledge of email marketing went no further than publishing a newsletter.</p>

<p>At the time, I set up a Mailchimp account, and (very) rarely published a newsletter with what I thought would be interesting information for the slowly growing number of subscribers.</p>

<p>As things got busier, though, the newsletter became less and less frequent, and when it was published, the open/read rates were low.</p>

<h2 id="discovering-email-marketing">Discovering Email Marketing</h2>

<p>Somewhere along the line, I'd heard about email marketing tools, but paid them little attention. I was already in <a href="https://nathanbarry.com/">Nathan Barry's</a> audience as a customer of the Designing Web Applications book, and was loosely following his journey building ConvertKit.</p>

<p>About 2 years ago, tired of managing Mailchimp lists and emails, I decided to give other platforms a go. I quickly settled on ConvertKit. The simple web-based UI made building email sequences and forms really easy, and it integrated well with Gumroad, the e-commerce site I use to sell my digital books and courses.</p>

<p>If you're looking to start building an audience for your content, <a href="https://convertkit.com">ConvertKit</a> is an excellent choice. It provides enough power "out of the box" to build landing pages, forms, email courses and more.</p>

<p>The most difficult part of using ConvertKit (for me, at least) is knowing what content to write! Let's look at how you can get started building your audience and sharing your content.</p>

<h2 id="building-a-simple-newsletter-with-convertkit">Building a Simple Newsletter with ConvertKit</h2>

<p>After signing up to <a href="https://convertkit.com">ConvertKit</a> (they offer a 14-day free trial), you'll be presented with the option to build Forms, Landing Pages and Sequences.</p>

<p>Forms are used to capture leads and emails on your website (e.g. signing up for a newsletter or special offer), whilst landing pages provide a fully-hosted web page on which you can add text and images to describe your service or offer.</p>

<p>The landing page includes an integrated form to capture leads, and ConvertKit have recently launched a new landing page editor and some great default theme templates.</p>

<p><img src="https://assets.chrisblunt.com/2018/08/landing-page.png" alt=""></p>

<h2 id="building-a-newsletter-sign-up-page">Building a Newsletter Sign-up Page</h2>

<p>Let's start by building a newsletter sign-up page. The latest release of <a href="https://convertkit.com">ConvertKit</a> provides a great template for us to do this.</p>

<p>Here's an example of the landing page you'll build (<a href="https://pages.convertkit.com/64d58cc4ba/cbcf76149b">see the original</a>):</p>

<p><img src="https://assets.chrisblunt.com/2018/08/landing-page-screenshot.png" alt=""></p>

<p><strong>→ Click on Forms along the top of the screen, then Create Form</strong></p>

<p>You'll be asked if you want to create a form or landing page. Let's build a complete landing page, which you can share to readers.</p>

<p><img src="https://assets.chrisblunt.com/2018/08/2018-08-14-convertkit-1.gif" alt=""></p>

<p><strong>→ Click Landing Page, then choose the Profile Template</strong></p>

<p>The Profile template is designed for a personal newsletter (although you tweak the content for anything, really).</p>

<p>The next screen you'll see is the page editor. You can click on the different elements to edit their content and colours. For example, click on <strong>Your name</strong> and change it to your own name.</p>

<p><strong>→ Click on "Your Name" and replace the text with your own name.</strong></p>

<p>You'll also see a sidebar open up, where you can edit the font style (colour, size, etc.) of the text.</p>

<p>Edit the other elements on the page until you're happy with the design. You can also click on the image selector to choose a profile image for your page.</p>

<p><img src="https://assets.chrisblunt.com/2018/08/2018-08-14-convertkit-2.gif" alt=""></p>

<p><strong>→ Click on the "Magic Wand" icon to choose your own colour scheme</strong></p>

<p>The styles panel allows you change various colours on your page. You can also change the colour of the Subscribe button by clicking  on it.</p>

<p><strong>→ Click the envelope icon to configure your double opt-in / incentive email.</strong></p>

<p><img src="https://assets.chrisblunt.com/2018/08/2018-08-14-convertkit-4.gif" alt=""></p>

<p>The incentive email panel allows you to customise (and enable/disable) the double opt-in verification email that your subscribers receive when signing up to your newsletter.</p>

<p><em>For compliance with GDPR for EU citizens, I recommend you always enable a double opt-in email.</em> <a href="https://convertkit.com"><em>ConvertKit</em></a> <em>also has GDPR options which you can manage in your account settings</em></p>

<p>In this screen, <a href="https://convertkit.com">ConvertKit</a> also allows you to upload an incentive attachment that will be downloaded automatically when the subscriber verifies their email address.</p>

<h2 id="connecting-your-form-to-a-sequence">Connecting Your Form to a Sequence</h2>

<p>Once you've built your landing page, the next step is to connect it to an email <em>sequence</em>. Sequences are chains of emails that are delivered to your subscribers on a timed basis. They are great for building evergreen newsletters, and also simple email courses.</p>

<p>Let's start by building a welcome email for our sequence.</p>

<p><strong>→ Click Sequences, then Create Sequence</strong></p>

<p><a href="https://convertkit.com">ConvertKit</a> will ask you to name your sequence. We'll call it <strong>Newsletter</strong> for now (you can always change this later).</p>

<p><img src="https://assets.chrisblunt.com/2018/08/2018-08-14-convertkit-3.gif" alt=""></p>

<p><strong>→ Write an introductory email</strong></p>

<p>The <a href="https://convertkit.com">ConvertKit</a> email editor is simple yet powerful. It let's you create great looking emails without having to worry about coding HTML or advanced templates.</p>

<p>ConvertKit's emails are designed to be read, so they look more like personal written emails than traditional, graphics-heavy newsletters you might be familiar with.</p>

<p><strong>→ Set sending options</strong></p>

<p>Finally, we need to mark our email as <em>published</em> by changing it's status in the drop-down menu.
As this is a welcome email, we should also make sure it delivers immediately to subscribers once
they have confirmed their email address.</p>

<p>To do this, click the <strong>after 1 day</strong> setting under <em>When to Send</em>, and enter <strong>0</strong> into
the field. This will remove any delay before the email is sent.</p>

<h2 id="putting-everything-together">Putting Everything Together</h2>

<p>ConvertKit offers a couple of ways to connect forms and sequences: <strong>Rules</strong> and <strong>Automations</strong>. Automations allow us to design an advanced flow-chart style set of rules for our subscribers to follow.</p>

<p>I'll cover automations in a future post. For now, though, we'll use a simple rule to connect our form to the sequence, so that when somebody subscribes to our form, the rule automatically subscribes them to our sequence.</p>

<p>Rules are a great way to build <em>If this happens, then do that</em> conditions.</p>

<p><strong>→ Click Automations, Rules, then Add Rule</strong></p>

<p>ConvertKit presents "Triggers" on the left and "Actions" on the right. When a trigger occurs, the corresponding Action(s) will run.</p>

<p><img src="https://assets.chrisblunt.com/2018/08/2018-08-14-convertkit-5.gif" alt=""></p>

<p><strong>→ Click the Subscribes to a Form trigger</strong></p>

<p>In the drop-down list that appears, choose the <strong>Profile landing page</strong> you created before.</p>

<p><strong>→ Click the Subscribe to a Sequence action</strong></p>

<p>In the drop-down list, choose the <strong>Newsletter</strong> sequence.</p>

<p><strong>→ Click Save Rule to create the rule.</strong></p>

<p>ConvertKit will now show your rule, along with a green <strong>Enabled</strong> light to show that rule will process when the trigger occurs.</p>

<h2 id="your-newsletter-is-ready">Your Newsletter is Ready!</h2>

<p>That's it! Your newsletter is now ready to go! You can try out your new rule by signing up to your landing page - just open the link that <a href="https://convertkit.com">ConvertKit</a> generates for your landing page, and enter your email address.</p>

<p>After confirming your subscription, you'll receive your new Welcome email!</p>

<h2 id="next-up">Next up...</h2>

<p>I hope you've found this guide helpful. To get started with ConvertKit, <a href="https://convertkit.com"><strong>click here to sign up and start your 14-day free trial</strong></a>.</p>

<p>In upcoming posts, I'll look at some of the more advanced techniques I've learned when using ConvertKit, including how to build more complex automation flows.</p>

<p>As part of this <em>Business Tools</em> series, I'll explore some of the other systems I use in the day-to-day running of my business.</p>

<p>Let me know your thoughts and comments by <del>tweeting me or</del> <a href="https://www.chrisblunt.com/contact/">getting in touch via email</a>.</p>
]]>
      </description>
      <pubDate>Tue, 14 Aug 2018 12:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/business-tools-how-to-get-started-with-convertkit-for-email-automation</link>
      <guid>https://www.chrisblunt.com/business-tools-how-to-get-started-with-convertkit-for-email-automation</guid>
      <category>automation</category>
      <category>business</category>
      <category>convertkit</category>
      <category>email</category>
      <category>marketing</category>
      <category>tools</category>
    </item>
    <item>
      <title>Rails on Docker: Using Rails Encrypted Credentials with Docker</title>
      <description>
        <![CDATA[<p>Rails 5.1 introduced the encrypted <code>secrets.yml.enc</code> file, with Rails 5.2
tidying things up by consolidating secrets and credentials into the
<code>credentials.yml.enc</code> file.</p>

<p>Along with the changes came the <code>secrets:edit</code> task, allowing you to edit
the credentials YAML file whilst automatically decrypting and encrypting the
file using a master key.</p>

<p>The key is provided either through <code>config/master.key</code> or supplying an
<code>RAILS_MASTER_KEY</code> environment variable. (More on this below…)</p>

<h2 id="editing-credentials-in-your-docker-container">Editing Credentials in your Docker Container</h2>

<p>When using Docker to develop and run your apps, though, getting <code>secrets:edit</code> to work may requires a few tweaks to your Docker image.</p>

<p>Depending on how the image is configured, you'll need to ensure that an editor
is installed in your container.</p>

<p>For one-off editing, we can do this using a simple command line:</p>
<pre data-language="shell">$ cd /path/to/your/app
$ docker run --rm -it --mount type=bind,src=${PWD},target=/app my_app /bin/sh -c 'apt update &amp;&amp; apt install -y vim &amp;&amp; EDITOR=vim bin/rails credentials:edit'</pre>
<p>Alternatively, you can ensure that vim (or your editor of choice) is installed
into the Docker image:</p>
<pre data-language="dockerfile">FROM ruby:2.5

RUN apt update -qq &amp;&amp; apt install -y vim # nano ...&lt; your choice of editor

# ... continue your Dockerfile</pre><pre data-language="shell">$ docker build -t my_app .
$ docker run --rm -it --mount type=bind,src=${PWD},target=/app -e EDITOR=vim my_app bin/rails credentials:edit # Assume your app's code is ADDed to /app in the Dockerfile.</pre>
<p>Using this technique, you can use Rails' built-in credentials editor
without the need to install rails itself (and other dependencies) on your
workstation machine.</p>

<h2 id="using-docker-compose">Using Docker Compose</h2>

<p>If you're using Docker Compose to manage your containers, credentials can be edited
in the same way using the <code>docker-compose run</code> command.</p>

<p>Assuming a declaration similar to the following in your <code>docker-compose.yml</code>:</p>
<pre data-language="yaml"># ...
services:
  web:
    build: .
    volumes:
      - .:/app
    # ...</pre><pre data-language="shell">$ docker-compose run --rm -e EDITOR=vim web bin/rails credentials:edit</pre>
<h2 id="supplying-your-master-key-as-an-environment-variable">Supplying Your Master Key as an Environment Variable</h2>

<p>By default, Rails creates a <code>config/master.key</code> file which is used to decrypt/encrypt
your credentials file. As this file is by default excluded from your code
repository, you may need to supply the key as an environment variable to your
containers:</p>
<pre data-language="shell">$ docker run --rm -it --mount type=bind,src=${PWD},target=/app -e EDITOR=vi -e RAILS_MASTER_KEY=your-master-key my_app bin/rails credentials:edit</pre>
<p><em>Note that this is only necessary if you do not have the <code>config/master.key</code>
file in your workspace</em></p>
]]>
      </description>
      <pubDate>Thu, 26 Jul 2018 16:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-on-docker-rails-encrypted-secrets-with-docker</link>
      <guid>https://www.chrisblunt.com/rails-on-docker-rails-encrypted-secrets-with-docker</guid>
      <category>credentials</category>
      <category>docker</category>
      <category>rails</category>
      <category>ruby</category>
      <category>secrets</category>
    </item>
    <item>
      <title>Value Pricing Mistakes (and what I’ve learned from them)</title>
      <description>
        <![CDATA[<p><em>At the start of my journey to value pricing, my goal was to become a 100% value pricing organisation. I’ve succeeded in that goal so far, but I’ve made plenty of mistakes along the way, and learned a few important lessons.</em></p>

<p>I hope that by documenting them here, you’ll be able to learn from my mistakes to make your journey to value pricing that little bit easier.</p>

<p>Above all, though, remember that it’s inevitable that moving entirely away from time billing is challenging — you are going against very established norms.</p>

<h1 id="1-not-recognising-value">1. Not Recognising Value</h1>

<p>This is perhaps the single mistake I’ve made in value pricing that underpins all others, and (anecdotally, at least) seems to be very common.</p>

<p>Recognising what your customer values, and — just as importantly — what they do not value, is absolutely critical to successful value pricing.</p>

<p>That does feel like stating the obvious, and on the surface, value pricing does sound pretty obvious. But as I’ve mentioned before, <a href="https://www.chrisblunt.com/conquering-the-fear-of-a-fixed-price">value is often very subjective</a>, and based on situation. It is often difficult to put into words, let alone numbers, what the value of something may be.</p>

<p>There have been many circumstances where I’ve struggled to identify value, and I know this is because I’ve failed to fully carry out the value conversation. This has led to lazy fixed-price billing, which is far from ideal.</p>

<p>I’ve always packaged the outcomes into a fixed price, though, and have managed to avoid time-based billing. In those situations where I’ve been tempted to apply a time-based rate as a “quick fix”, I’ve avoided it by reminding myself <a href="https://www.chrisblunt.com/why-you-should-stop-charging-for-your-time">how time-billing is painful for everyone involved</a>.</p>

<p>To help avoid this lazy pricing, I’ll continue to productise all of my service offerings, and gradually establish systems that encourage a lot more focus on that in-depth value conversation.</p>

<p>However — and as difficult as it has been for me to accept— there are a couple of situations I’ve come across where this more approach to projects isn’t the right fit, and where value pricing may not be the best approach…</p>

<h1 id="2-value-pricing-everything">2. Value Pricing Everything</h1>

<p>My goal was to become 100% value pricing, but there have been a couple of situations where I’ve really struggled to do much more than offer a fixed price:</p>

<h2 id="minor-maintenance-tasks">Minor Maintenance Tasks</h2>

<p>These are the small and often rush/panic jobs that come in such as fixing a minor bug or tweaking a layout.</p>

<p>The technical effort is usually lower and there’s no value in discussing any “bigger picture” subjects (at this stage).</p>

<p>It is sometimes difficult to remember when you spend most of your time in code, but as Jonathan Stark reminds us on <a href="http://www.ditchinghourly.com/398c5403">this episode of his podcast</a>, code has no value to your customer:</p>

<p>My approach to this type of quick-maintenance work has been to wrap it <a href="https://www.plymouthsoftware.com/ruby-on-rails-micro-consultation/">into a fixed-price, limited-scope product</a>. There are some obvious risks in doing this, but that is reflected in the price/value to me.</p>

<p>Productising the process also greatly simplifies things for the customer, and allows them to quickly weigh up their options.</p>

<p>The <em>CodeShot</em> productised service has been particularly well received, and has led on to further projects. In this way, it acts as a great introductory service for the customer, and is now one of the first things I’ll recommend to new customers who are looking at larger or longer-term projects.</p>

<h2 id="agency-style-work">Agency-style work</h2>

<p>In this style of work, you are being taken on as simply a “pair of hands” to top up an existing workforce. Although I’ve said agency-style work, your client may not be an <em>actual</em> agency.</p>

<p>For example, you might be contracted in to a larger organisation’s team, but the model is similar. Generally, I’m considering work as agency-style if you could easily be mistaken for, or replaced by an employee, and have little or no interaction with the end-customer.</p>

<p>In this situation, <em>your</em> customer is the agency. The value to them is in maximising their profit on your effort, and so value conversations are difficult (simply put, the maximum value to them is minimising your price). This leads to normal market pressures and a focus on input effort rather than outcomes.</p>

<p>I’ve previously tried to apply value pricing to these types of engagement, but it has never been particularly successful. I’ve also used a more productised approach (such as <em>CodeShots</em>) to handle smaller jobs. But as much as it is difficult to admit, I think there is little point in trying to apply value pricing to agency-style work.</p>

<p>As I don’t tend to do a lot of this type of work, my approach moving forward is simply to apply a minimum daily or weekly rate based on my perceived value, and accept the <a href="https://www.chrisblunt.com/why-you-should-stop-charging-for-your-time">issues that come with that</a>.</p>

<p>Of course, the priority is to continue finding and building projects where I am working directly with the end-customer, as this allows me to deliver far more value, and work on a partnership level with them and their business.</p>

<p><em>I’d love to <a href="https://www.chrisblunt.com/contact/">hear from you</a> if you have successfully value-priced agency style work.</em></p>

<h1 id="3-underestimating-valuable-outcomes">3. Underestimating Valuable Outcomes</h1>

<p>Closely linked with not <em>recognising value</em> above, I still find myself underestimating the true value of outcomes to the customer, even when I know what those outcomes are (i.e. I’ve recognised them, but underestimated their impact).</p>

<p>I’m getting better with this as time goes on — it helps to remember value pricing is a journey, so adjustments can be made with time. Productising my services has certainly been one of the best tools to help do this.</p>

<p>Sometimes, though, it can be very much a case of not seeing the wood for the trees.</p>

<p>Ironically, this is not the case when looking at other businesses from the outside in . Often when working with customers, I’ve found it easy to spot <em>value leaks</em> in their businesses, and can quantify that value without question. Yet doing the same in my own business seems almost impossible!</p>

<p>Perhaps it helps being an outsider to the business, and also being able to appreciate what their valuable outcomes might be worth to me as a customer.</p>

<p>I’m fairly sure I’m not alone in this, and am considering business coaching or mentorship to help put a second pair of eyes on my business and help spot any value leaks.</p>

<p>Having a mastermind group, such as <a href="http://artofvalue.com/">Kirk Bowman’s Art of Value Society</a> is a great way to bounce value pricing ideas and challenges.</p>

<hr>

<p>Would you like to get another perspective on your software business? As I’m considering doing exactly this for my own business, I’d love to help others who are on the journey to value pricing.</p>

<p>Start a discussion by <a href="https://www.plymouthsoftware.com/contact">getting in touch</a>.</p>
]]>
      </description>
      <pubDate>Wed, 21 Mar 2018 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/value-pricing-mistakes-and-what-ive-learned-from-them</link>
      <guid>https://www.chrisblunt.com/value-pricing-mistakes-and-what-ive-learned-from-them</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Should You Track Your Time When Value Pricing?</title>
      <description>
        <![CDATA[<p><em>One of the goals of value pricing as a service business is to break the link between the price you charge and the time you put into a project. But should you still track your own time even when you’re not using it to price your services?</em></p>

<p>A value-based price is one that reflects the outcomes that your customer will receive as a result of your inputs. The effort, time and energy required for those outcomes isn’t relevant to the price.</p>

<p><em>Your customer places little, if any, value on your time.</em></p>

<p>As a knowledge-based professional, this can be a tough pill to swallow. After all, you’ve spent years learning your craft; and the amount of time you put into a project is likely to be very valuable to you.</p>

<p>So should you be tracking your time, even if you’re pricing based on value?</p>

<h1 id="time-is-an-operational-cost-of-business">Time is an Operational Cost of Business</h1>

<p>Regardless of how you price your services, you need to see yourself as an <em>operational</em> cost of your business.</p>

<p>And just like any cost, you need to make sure that you are providing a return for the business; ultimately, you need to be profitable for your business.</p>

<p>Value-pricing purists might say time tracking can be abandoned, but despite every effort, I’ve really struggled with this. I’m still not quite there yet, and throughout my transition to value pricing, I’ve continued to track my time (using <a href="https://toggl.com/">Toggl</a>) for pretty much everything I do in my business.</p>

<p>This includes not only “billable” work, but also internal tasks, such as updating accounts, filling in tax returns, writing <a href="https://www.chrisblunt.com/courses">books and courses</a>, marketing, and so on.</p>

<p>I’m even tracking the time I spend writing and editing this article (1h 25m).</p>

<p>Why would I do this? After all, keeping track of time is an inconvenience at best.</p>

<p>But keeping an track of how I spend my time allows me to know my cost to the business, as well as giving me the opportunity to identify inefficiencies and delegate tasks that get in the way of more valuable work.</p>

<p>Whilst it may be irrelevant to your customer, I think knowing how you spend your time is still important for you and your business.</p>

<h1 id="time-is-a-floor-to-but-does-not-determine-price">Time is a floor to (but does not determine) price</h1>

<p>Despite tracking my time, it’s crucial to remember that it is not used to determine the value-based price of a service, except perhaps as a baseline, i.e. a break-even point.</p>

<p>After all, you want to ensure that your business creates a profit on your input.</p>

<p>If you work solo, it can be easy to forget this — you almost have to see yourself as two separate entities: the business owner and a separate, salaried team member.</p>

<p>When determining a value-based price, regardless of the value to the customer, you need to ensure you are covering your operating costs. Tracking your time allows you to benchmark different jobs and help provide a guesstimate of how long a piece of work might take.</p>

<p>As you begin to <a href="https://www.chrisblunt.com/3-steps-to-start-value-pricing">productise your services</a>, tracking your time can also help you monitor efficiency.</p>

<p>Remember, though, that the value of the outcomes you deliver should be several orders of magnitude above your operating costs, and at least several times the price your charge.</p>

<p>This can often be difficult to determine; if you’re struggling to justify (to yourself) a value based fee that meets this criteria, then it’s likely you’ve mis-identified where the value lies for the customer; or — and it has <a href="https://www.chrisblunt.com/value-pricing-mistakes-and-what-ive-learned-from-them">taken me a long while to accept this</a> — it may be that the work is simply too small or not suitable for a value based price.</p>

<p>This is a subject I’ll cover in an upcoming post, but generally, the former can be resolved by continuing the value conversation with your customer. The latter might actually be better resolved with a different pricing model or, preferably, offering a value-priced, productised service that fulfils the needs of the client whilst maintaining a profit margin for your business.</p>

<h1 id="conclusion">Conclusion</h1>

<p>As a business owner, it’s essential to understand your operating costs and where (in)efficiencies lie in your business.</p>

<p>With this in mind, should you track your time when adopting value-based pricing in your business?</p>

<p>My thinking on this may change in the future, but at the moment I would say yes. With the strict caveat that time reports are “for your eyes only”. Your timesheet should never be a concern for your customers, nor used as the basis for pricing your services.</p>

<hr>

<p>Would you like to get another perspective on your software business? As I’m considering doing exactly this for my own business, I’d love to help others who are on the journey to value pricing.</p>

<p>Start a discussion by <a href="https://www.plymouthsoftware.com/contact">getting in touch</a>.</p>
]]>
      </description>
      <pubDate>Wed, 21 Mar 2018 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/should-you-track-your-time-when-value-pricing</link>
      <guid>https://www.chrisblunt.com/should-you-track-your-time-when-value-pricing</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Ruby on Rails: 5 Checks to Make Before Launching Your App</title>
      <description>
        <![CDATA[<p>Launching your app into production is a huge step, and there are often lots of
different things you need to set up to ensure not only that the deployment works
as expected, but also that your app continues to run smoothly once it is running
in production.</p>

<p>Here are five things I like to check prior to deploying a Ruby on Rails app:</p>

<h3 id="1-customise-the-pages-your-users-shouldn-t-really-see">1. Customise the pages your users shouldn't really see</h3>

<p>It's inevitable that at some point, your app will throw an error page - be it a server
issue (500) or even a missing page. Rails provides some basic templates for these pages,
but they aren't very useful to users.</p>

<p>Instead, you should customise the <code>public/404.html</code> and <code>public/500.html</code> pages to provide
some useful feedback to site users, and even an <em>escape route</em> (such as a link to your homepage or site search) to help them resolve their issue.</p>

<h3 id="2-ensure-the-app-enforces-an-encrypted-connection">2. Ensure the app enforces an encrypted connection</h3>

<p>With the arrival of <a href="https://letsencrypt.org">LetsEncrypt</a>, there really is no excuse not to use an SSL certificate for your apps now. Even the most basic sites benefit from the security that an SSL certificate provides to its users.</p>

<p>You can enforce SSL easily across your app by setting in your environment configuration file:</p>
<pre data-language="ruby"># config/environments/production.rb
# ...
  config.force_ssl = true</pre>
<h3 id="3-don-t-leak-database-details-through-your-urls">3. Don't leak database details through your URLs</h3>

<p>The default behaviour of Rails routing is to use database ID fields for URLs. For example, to edit a user's account, the URL might be <code>yourapp.com/users/5/edit</code>. Using the database fields in this way could potentially leak data from your application, and offer a potential security vulnerability in your application if (for example) an authorization check is not correctly carried out on the action.</p>

<p>An easy workaround is to use a separate, auto-generated UUID field on your model, and override the model's <code>to_param</code> method:</p>
<pre data-language="ruby">class User &lt; ApplicationRecord
  has_secure_token :uuid # Ideally, add a UNIQUE index to this field on your database

  def to_param
    self.uuid
  end
end</pre>
<p>Alternatively, a more complete approach is to use the excellent <a href="https://github.com/norman/friendly_id/">FriendlyId</a> gem to take care of this easily. FriendlyID also gives you the benefit of being able to generate URL-friendly slugs for your model, and redirections should they change.</p>

<h3 id="4-check-for-availability">4. Check for Availability</h3>

<p>Once your app is up and running, you want to make sure it stays that way! This means monitoring the various parts that make up your app. Thankfully, there are a number of services to make this task easy.</p>

<p>For example, <a href="https://www.statuscake.com/">StatusCake</a> and <a href="https://www.pingdom.com/">Pingdom</a> perfor regular checks on your website, and can even be configured to ensure that the response includes a particular string of text (e.g. "Welcome to my App"). This helps to ensure not only that your app is responding correctly, but that (for example) users are not seeing unexpected content (such as a directory listing!)</p>

<h3 id="5-automate-your-deployment">5. Automate your Deployment</h3>

<p>Automating your configuration and deployment is one of the best steps you can take to keeping your app stable in production. CI/CD automation tools can be configured to perform a number of checks on your code both during and after the deployment to ensure that everything has gone smoothly.</p>

<p>By setting this up once, you can be far more confident about the state of your application and its services every time you make a change. There are a number of CI / CD tools available, and the best one for you will depend on your particular setup.</p>

<p>However, to get started, I highly recommend checking out either <a href="https://github.com/drone/drone">Drone</a>, or <a href="https://docs.gitlab.com/ce/ci/">Gitlab CI</a> (especially if you are already using a Gitlab instance to host your code repository).</p>

<h2 id="get-the-30-step-pre-launch-checklist-for-healthy-rails-apps">Get the 30-Step Pre-Launch Checklist for Healthy Rails Apps</h2>

<p>These are just 5 things you should check as part of your app's launch into production, but there are plenty more!</p>

<p>To help you out, I've put together a handy reference worksheet that includes 30-step checklist to step through when preparing your app. You can download the checklist using the form below.</p>


]]>
      </description>
      <pubDate>Mon, 12 Mar 2018 11:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/ruby-on-rails-5-checks-before-launching-your-app</link>
      <guid>https://www.chrisblunt.com/ruby-on-rails-5-checks-before-launching-your-app</guid>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Ruby on Rails: Running Tests with Guard and Docker</title>
      <description>
        <![CDATA[<p><a href="https://github.com/guard/guard">Guard</a> is a great tool for quickly running your Rails app tests/specs as your develop the code. As you edit and change the files in your app, Guard can trigger tests specific to the files you are modifying.</p>

<p>If you're using Docker to develop your app, however, running those tests/specs can be time consuming. Spinning up a container for every run soon becomes time consuming and wastes resources.</p>

<p>A better approach during development is to use a container for your app that is running Guard. This container will monitor changes to your files and run your tests within itself.</p>

<p>Whilst there have previously been issues with Guard picking up file changes across the Docker mount (<a href="https://www.sitepoint.com/guard-and-docker/">due to the underlying use of VirtualBox</a>, for example), recent releases of Docker (for Mac) seem to have resolved the issue.</p>

<p>In this tutorial, you'll see how to use a Docker container to run Guard and monitor your app's code for changes.</p>

<h2 id="a-basic-dockerised-rails-app">A Basic Dockerised Rails App</h2>

<p>Starting with an app that has some tests (minitest) or specs (rspec) in place, add the <code>guard</code> gem and appropriate plugins for your test suite (e.g. <code>guard-minitest</code> and/or <code>guard-rspec</code>):</p>
<pre data-language="ruby"># Gemfile
group :development do
  gem 'guard'
  gem 'guard-minitest'
  gem 'guard-rspec' # If you use Rspec instead of Minitest
end</pre>
<p>If your Rails app doesn't already have a <code>Dockerfile</code>, the following example is a good starting point. You might need to include more packages depending on your app's specific needs. See <a href="https://www.chrisblunt.com/rails-on-docker-getting-started-docker-ruby-rails/">Getting Started with Docker and Ruby on Rails</a> for more details about how to set this up.</p>
<pre data-language="dockerfile"># Dockerfile
# /path/to/your/app/Dockerfile
FROM ruby:2.4.3

RUN apt-get update -qq &amp;&amp; apt-get install -y nodejs

# Cache Gems
WORKDIR /tmp
ADD Gemfile .
ADD Gemfile.lock .

RUN bundle install --jobs 4

# Copy your app's code into the image
WORKDIR /usr/src/app
ADD . /usr/src/app

# Precompile assets
RUN bundle exec rails assets:precompile

# RUN apt-get install -y libreadline6 libreadline6-dev

# Expose port 3000 to other containers
ENV PORT 3000
EXPOSE $PORT

# Run the rails server
CMD rails s -b 0.0.0.0 -p $PORT</pre>
<p>When your Dockerfile is ready, you can build (and tag) an image from it. In this tutorial, the image tag will be <code>myapp:latest</code>:</p>
<pre data-language="shell">$ docker build -t myapp:latest .</pre>
<p>Next, check your suite runs by running your tests/specs inside a Docker container:</p>
<pre data-language="shell">$ cd /path/to/your/app
$ docker run --rm -it -v ${PWD}:/usr/src/app myapp:latest bin/rails test # or bin/rails spec

Running via Spring preloader in process 19
Run options: --seed 61489
# Running:
...
Finished in 1.462146s, 2.0518 runs/s, 6.1553 assertions/s.
3 runs, 9 assertions, 0 failures, 0 errors, 0 skips</pre>
<h2 id="installing-guard">Installing Guard</h2>

<p>With our tests running inside Docker, it's time to set up Guard. Again, this can be done using the existing Docker image for your app:</p>
<pre data-language="shell">$ docker run -it -v ${PWD}:/usr/src/app --rm myapp:latest bundle exec guard init minitest rspec</pre>
<p>Here, I've included both the <code>minitest</code> and <code>rspec</code> guard plugins, but you might only want to include the plugin that is relevant for your app.</p>

<p>Your app's folder will now include a <code>Guardfile</code>. Open it up, and edit according to your needs. For example, if you're using Rspec, the default configuration will look like:</p>
<pre data-language="ruby"># Guardfile
guard :rspec, cmd: "bundle exec rspec" do
  require "guard/rspec/dsl"
  dsl = Guard::RSpec::Dsl.new(self)

  # Feel free to open issues for suggestions and improvements
  ...
end</pre>
<p>See the README file for your plugin(s) to learn about their various configuration options.</p>

<h2 id="run-guard">Run Guard</h2>

<p>With Guard configured, we can now spin up a new container that is running Guard and monitoring the app's code. Be sure to mount your code folder into the container, so that changes are applied as you make them:</p>
<pre data-language="shell">$ docker run -it -v ${PWD}:/usr/src/app --rm myapp:latest bundle exec guard</pre>
<p>The container will attach to Guard as if it was running on your host machine. For example, you can press enter to run all your tests:</p>
<pre data-language="shell">[1] guard(main)&gt; # Enter
12:09:56 - INFO - Run all
12:09:56 - INFO - Running all specs
....

Finished in 0.39492 seconds (files took 4.05 seconds to load)
4 examples, 0 failures</pre>
<p>Now if you edit one of your test/spec files, Guard should pick up the change and automatically run your test/spec, as if it was running locally!</p>

<p>To exit Guard (and remove the container), just type <code>exit</code> or hit <strong>Ctrl+D</strong></p>

<h2 id="adding-guard-to-your-compose-file">Adding Guard to Your Compose File</h2>

<p>Now that we know Guard is working and monitoring file changes inside our Docker container, you can add a declaration to your Docker Compose file. This will allow you to run Guard automatically as part of your development suite when you call <code>docker-compose up</code>.</p>

<p>One caveat, though, is that we can't use Guard's interactive console when calling <code>docker-compose up</code>. The process does not attach to the console, and so the container immediately fails. A workaround for this is to add the <code>--no-interactions</code> argument to the container's command:</p>
<pre data-language="yaml"># docker-compose.yml
version: '3.2'

services:
  web:
    build: .
    ports:
      - '3000:3000'
    volumes:
      - .:/usr/src/app
    environment:
      - RAILS_ENV=development
    # ...

  guard:
    build: .
    volumes:
      - .:/usr/src/app
    environment:
      - RAILS_ENV=development
    command: bundle exec guard --no-bundler-warning --no-interactions</pre>
<p>Now when you call <code>docker-compose up</code>, you will see the Guard container start up and waiting for notifications from the filesystem:</p>
<pre data-language="shell">$ docker-compose up
guard_1  | 12:23:39 - INFO - Guard::RSpec is running
guard_1  | 12:23:39 - INFO - Guard is now watching at '/usr/src/app'
web_1    | =&gt; Booting Puma
web_1    | =&gt; Rails 5.1.4 application starting in development
web_1    | =&gt; Run `rails server -h` for more startup options
web_1    | Puma starting in single mode...
web_1    | * Version 3.10.0 (ruby 2.4.3-p205), codename: Russell's Teapot
web_1    | * Min threads: 5, max threads: 5
web_1    | * Environment: development
web_1    | * Listening on tcp://0.0.0.0:3000
web_1    | Use Ctrl-C to stop</pre>
<p>As before, if you now edit one of your test/spec files, you'll see Guard triggers the appropriate tests:</p>
<pre data-language="shell">12:24:16 - INFO - Running: spec/mailers/admin_mailer_spec.rb
guard_1  | ....
guard_1  |
guard_1  | Finished in 0.6642 seconds (files took 3.75 seconds to load)
guard_1  | 4 examples, 0 failures</pre>
<p>With everything working, you can now continue building your app as before, safe in the knowledge that whilst it is running, Guard is monitoring your tests, and will flag any breaking tests.</p>
]]>
      </description>
      <pubDate>Mon, 19 Feb 2018 00:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/ruby-on-rails-running-tests-with-guard-and-docker</link>
      <guid>https://www.chrisblunt.com/ruby-on-rails-running-tests-with-guard-and-docker</guid>
      <category>docker</category>
      <category>guard</category>
      <category>minitest</category>
      <category>rails</category>
      <category>rspec</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Rails on Docker: Using Yarn to Manage Frontend Assets</title>
      <description>
        <![CDATA[<p>Rails 5.1 introduced the front-end package manager Yarn into the standard tooling for new Rails projects. Yarn allows you to declare and manage your app's frontend dependencies (such as CSS frameworks and Javascript libraries) in a way similar to how Bundler manages Rails dependencies (gems).</p>

<p>If you're using Yarn to manage your app's frontend libraries, you'll need to ensure that Yarn is available to use when adding or precompiling your assets in your Docker container. To do this, you'll need to add a few extra lines to your app's Dockerfile.</p>

<p>Let's start by creating a new Rails app, and creating a basic Dockerfile for our app's image:</p>
<pre data-language="shell">$ rails -v # Make sure you are using &gt;= Rails 5.1
$ rails new my-app -d postgresql -B
$ cd my-app</pre>
<p>Create your <code>Dockerfile</code> and add the following:</p>
<pre data-language="dockerfile"># /path/to/app/Dockerfile
FROM ruby:2.3-alpine

# Set local timezone
RUN apk add --update tzdata &amp;&amp; \
    cp /usr/share/zoneinfo/Europe/London /etc/localtime &amp;&amp; \
    echo "Europe/London" &gt; /etc/timezone

# Install your app's runtime dependencies in the container
RUN apk add --update --virtual runtime-deps postgresql-client nodejs libffi-dev readline sqlite

# Install Yarn
ENV PATH=/root/.yarn/bin:$PATH
RUN apk add --virtual build-yarn curl &amp;&amp; \
    touch ~/.bashrc &amp;&amp; \
    curl -o- -L https://yarnpkg.com/install.sh | sh &amp;&amp; \
    apk del build-yarn

# Bundle into the temp directory
WORKDIR /tmp
ADD Gemfile* ./

RUN apk add --virtual build-deps build-base openssl-dev postgresql-dev libc-dev linux-headers libxml2-dev libxslt-dev readline-dev &amp;&amp; \
    bundle install --jobs=2 &amp;&amp; \
    apk del build-deps

# Copy the app's code into the container
ENV APP_HOME /app
COPY . $APP_HOME
WORKDIR $APP_HOME

# Configure production environment variables
ENV RAILS_ENV=production \
    RACK_ENV=production

# Expose port 3000 from the container
EXPOSE 3000

# Run puma server by default
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]</pre>
<p>Notice the lines to install Yarn. This will install Yarn into your Docker image, and update the $PATH variable so that it is available to use on the command line:</p>
<pre data-language="shell">$ docker build . -t my-app</pre>
<h2 id="add-frontend-depedencies">Add  Frontend Depedencies</h2>

<p>Now your app's Docker image is built with Yarn installed, you can use it to spin up a container and add a frontend dependencies to your app.</p>

<p>To demonstrate this, let's add the Tachyons CSS framework:</p>
<pre data-language="shell">docker run --rm -it --env RAILS_ENV=development --volume ${PWD}:/app my-app bin/yarn add tachyons

yarn add v1.3.2
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 2 new dependencies.
├─ tachyons-sass@4.8.1
└─ tachyons@4.9.0
Done in 2.10s.</pre>
<p>If you look inside your app's folder, you'll see that yarn has added a <code>yarn.lock</code> file and a <code>node_modules</code> folder.</p>

<p>The lockfile is used by Yarn to determine exactly which versions of dependencies to install (similar to your app's <code>Gemfile.lock</code> for gems), and <code>node_modules</code> is where those dependencies are saved.</p>

<p>Note that the <code>node_modules</code> folder should <em>not</em> be committed to your code repository (it is add to <code>.gitignore</code> by default).</p>

<p>You should also ensure it is not added to you Dockerfile when building by adding it to <code>.dockerignore</code>:</p>
<pre data-language="plain"># .dockerignore
node_modules/</pre>
<h2 id="using-frontend-dependencies">Using Frontend Dependencies</h2>

<p>Rails 5.1 is configured to search the <code>node_modules</code> folder by default when precompiling assets. This means we can easily use the Tachyon's CSS framework by calling it in <code>application.css</code>:</p>
<pre data-language="css">/* app/assets/stylesheets/application.css */

/**
 * ...
 * = require tachyons/css/tachyons.min.css # &lt;- Path relative to node_modules/
 * = require_tree .
 * = require_self
 */</pre>
<p>(Note you can see the <code>node_modules</code> folder added to the asset pipeline in <code>config/initializers/assets.rb</code>)</p>

<p>Next, create a simple <code>home</code> controller that we can use the build a demo page:</p>
<pre data-language="shell">$ docker run --rm -it --env RAILS_ENV=development --volume ${PWD}:/app my-app bin/rails g controller home index</pre>
<p>And finally, add some Tachyons CSS classes to your app's layout. For now, we'll just create a simple red header across the top of the page:</p>
<pre data-language="erb">    &lt;!-- app/views/layouts/application.html.erb --&gt;
    ...
    &lt;body class="ma0"&gt;
      &lt;header class="bg-dark-red ma0"&gt;
        &lt;div class="mw8 center"&gt;
          &lt;h1 class="f1 fw6 white ma0 pv3"&gt;Hello Rails&lt;/h1&gt;
        &lt;/div&gt;
      &lt;/header&gt;

      &lt;main class="mw8 center"&gt;
        &lt;%= yield %&gt;
      &lt;/main&gt;
    &lt;/body&gt;
    ...</pre><pre data-language="shell">$ docker run --rm -it --env RAILS_ENV=development --volume ${PWD}:/app --publish 3000:3000 my-app</pre>
<p>With your app running, let's check that everything looks as expected. Open <a href="http://localhost:3000/home/index">http://localhost:3000/home/index</a> in your browser and you should be greeted with the (basic!) styled layout:</p>

<p><img src="/uploads/2017/11/24/hello-rails-tachyons.png" alt="Hello Rails Tachyons CSS"></p>

<h2 id="finishing-up">Finishing Up</h2>

<p>That's all there is to using Yarn! By installing it inside your app's Docker container, you'll also be able to successfully precompile assets when deploying your app to CI or production servers.</p>

<p>Yarn is called as part of the <code>assets:precompile</code> task to ensure that frontend dependencies are available to the asset pipeline. You can see this in action by running the task through your container:</p>
<pre data-language="shell">$ rm -rf node_modules # Remove the existing node_modules folder first

$ docker run --rm -it --volume ${PWD}:/app --publish 3000:3000 my-app bin/rails assets:precompile
yarn install v1.3.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
Done in 0.72s.
I, [2017-11-24T10:12:57.634438 #1]  INFO -- : Writing /app/public/assets/application-dcfb333542166c7e176ac13945f5c27350627b932d212a870a5a112daf6d4db9.js
I, [2017-11-24T10:12:57.636782 #1]  INFO -- : Writing /app/public/assets/application-dcfb333542166c7e176ac13945f5c27350627b932d212a870a5a112daf6d4db9.js.gz
...</pre>
]]>
      </description>
      <pubDate>Fri, 24 Nov 2017 10:17:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/rails-on-docker-using-yarn-to-manage-frontend-assets</link>
      <guid>https://www.chrisblunt.com/rails-on-docker-using-yarn-to-manage-frontend-assets</guid>
      <category>docker</category>
      <category>rails</category>
      <category>ruby</category>
      <category>yarn</category>
    </item>
    <item>
      <title>Rails on Docker: Using Docker Compose with Your Ruby on Rails Apps</title>
      <description>
        <![CDATA[
  <strong>3 December 2018</strong>: Use <code>ruby:2.5-alpine</code> Docker images, and replace <code>links</code> with <code>depends_on</code> in <code>docker-compose.yml</code>.


<p>In the <a href="/rails-on-docker-getting-started-docker-ruby-rails">previous tutorial</a>, you learned how to get a simple Ruby on Rails app up and running on Docker containers.</p>

<p>As you work on your app, it often becomes difficult (and tiresome) to remember all the different configurations and arguments you need to create and link containers. The complexity increases as your app's dependencies grow (such as when you need to using Sidekiq and to handle background jobs).</p>

<p>Thankfully, Docker comes with a great solution to this problem in the form of <a href="https://docs.docker.com/compose/overview/">Docker Compose</a>.</p>

<p>Compose allows you to declare your app's requirements and configuration a simple yaml file. From this file, Docker will create, configure and start the containers. You can also use Compose to specify data volumes for your containers, and connect containers across an isolated virtual network.</p>

<p>In this tutorial, we'll take the simple Rails app from last time and use Compose  to replicate the container setup. This will make creating an "instance" of our Rails app much faster, and allow configurationt to be easily shared between developers.</p>

<hr>

<p><strong>Need the code?</strong> If you don't have the Rails application code from last time, you can create it as new Rails app using the following template:</p>
<pre data-language="shell">$ rails new --database=postgresql --skip-bundle --template=https://gist.githubusercontent.com/cblunt/1d3b0c1829875e3889d50c27eb233ebe/raw/8fce95533db3a3be19cb1aa31054589d219433c2/rails-docker-pg-template.rb my-app</pre>
<h2 id="creating-a-compose-file">Creating a Compose File</h2>

<p>The simple rails app we built makes use of two containers: one for the app itself and one for the PostgreSQL database. To run them, we created and linked two containers using the following commands:</p>
<pre data-language="shell">$ cd my-app
$ docker build . -t my-app
$ docker run -d -it --env POSTGRES_PASSWORD=superSecret123 --env DB_NAME=my-app_development --name mydbcontainer postgres:9.6
$ docker run --rm -it --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --publish 3000:3000 --volume ${PWD}:/app --link mydbcontainer:db my-app</pre>
<p>We'll recreate this setup in a new <code>docker-compose.yml</code> file:</p>
<pre data-language="yaml"># ./my-app/docker-compose.yml
version: '3'

services:
  db:
    image: postgres:9.6
    environment:
      - POSTGRES_PASSWORD=superSecret123
      - DB_NAME=my-app_development
  web:
    build: .
    environment:
      - RAILS_ENV=development
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=superSecret123
    ports:
      - '3000:3000'
    volumes:
      - .:/app
    depends_on:
      - db</pre>
<p>Notice that while the db container uses the <code>postgres:9.6</code> image, the <code>web</code> container (which is our app) will build a new docker image from the existing <code>Dockerfile</code> in your app's folder.</p>

<p>You can see that the YAML file reflects the command-line arguments we previously supplied to Docker. However, it is much easier to read and manage!</p>

<hr>

<h3 id="protip-what-are-services">ProTip: What are Services?</h3>

<p>Rather than specify each individual container, in Compose we specify <em>services</em>. Each service (<code>db</code>, <code>web</code> in the file above) can be thought of as representing one or more containers that running a particular Docker image.</p>

<p>Services allow us to create any number of identical containers running the same application. This is important when we come to scale services (i.e. run 2 or more copies of our Rails application and load-balance between them).</p>

<p>For this tutorial, we'll only be using one container per service so you can think of each <em>service</em> as a single container.</p>

<hr>

<h2 id="starting-services">Starting Services</h2>

<p>Next we'll use Docker Compose to create and start the containers (services) specified in the <code>docker-compose.yml</code> file. Compose will automatically download or build any images it needs to create each container:</p>
<pre data-language="shell">$ docker-compose up -d
Creating network "myapp_default" with the default driver
Creating myapp_db_1 ... done
Creating myapp_web_1 ... done</pre>
<p>Once your image is built, Compose will create and start the containers necessary for your app to run. The <code>-d</code> flag detaches your console allowing the containers to run in the background. You can tail the log messages from the containers just like you would with docker, but using the <code>docker-compose</code> command instead.</p>

<p>Compose will tail the logs for all of the containers specified in the <code>docker-compose.yml</code> file:</p>
<pre data-language="shell">$ docker-compose logs -f
Attaching to myapp_web_1, myapp_db_1
web_1  | Puma starting in single mode...
...
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop
...
db_1   | LOG:  database system was shut down at 2017-09-09 11:20:25 UTC
db_1   | LOG:  MultiXact member wraparound protections are now enabled
db_1   | LOG:  database system is ready to accept connections
db_1   | LOG:  autovacuum launcher started</pre>
<p>Hit [Ctrl-C] to stop tailing the logs. You can also show or tail logs from just one of the containers by using the name given to it in <code>docker-compose.yml</code>. For example, to tail only our app's logs:</p>
<pre data-language="shell">$ docker-compose logs -f web
web_1  | Puma starting in single mode...
...
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop</pre>
<p>Hit [Ctrl-C] to exit the logs (note if you omit the <code>-f</code> flag, the logs command will automatically exit)</p>

<p>You can also see the app is running by opening your browser and visiting <a href="http://localhost:3000/">localhost:3000</a>.</p>

<h2 id="running-single-task-containers-with-compose">Running Single-Task Containers with Compose</h2>

<p>Just as before, your app will throw a missing database error. This is because the container that was created by Compose is a completely separate instance from the previous container (remember that containers are ephemeral. As soon as they are removed, any data within them is lost).</p>

<p>As in the previous tutorial, we'll need to run a database migration task to create our app's database inside the <code>db</code> container. Just like normal Docker, we can use Compose to run create, use and remove a single-task container. Compose will take care of creating, configuring and running any dependent containers (e.g. the database container) as specified in the <code>docker-compose.yml</code> file before running the task.</p>

<p>To create and migrate our database, use the <code>docker-compose run</code> command (again, this is nearly identical to the standard <code>docker run</code> command:</p>
<pre data-language="shell">$ docker-compose run --rm web bin/rails db:create db:migrate
Starting myapp_db_1 ... done
Created database 'my-app_development'
Created database 'my-app_test'
== 20170909110604 CreatePosts: migrating ======================================
-- create_table(:posts)
   -&gt; 0.0221s
== 20170909110604 CreatePosts: migrated (0.0223s) =============================</pre>
<p>Specifying the <code>--rm</code> flag tells Compose to remove the container once the task has finished. Notice that although we used the web specification from our <code>docker-compose.yml</code> file, Compose created, used and removed a completely separate container to run this task. The previous instance of our <code>web</code> container is still running:</p>
<pre data-language="shell">$ docker-compose ps
   Name                  Command               State           Ports
-----------------------------------------------------------------------------
myapp_db_1    docker-entrypoint.sh postgres    Up      5432/tcp
myapp_web_1   bundle exec puma -C config ...   Up      0.0.0.0:3000-&gt;3000/tcp</pre>
<p>Now if you revisit <a href="http://localhost:3000">localhost:3000</a>, you'll see the familiar Posts screen.</p>

<h2 id="stopping-and-tidying-up-containers">Stopping and Tidying Up Containers</h2>

<p>Compose takes care of tidying up containers for us just as easily as creating them. You can stop and start containers in the normal way. This will stop but not remove them, allowing them to be restarted with data intact:</p>
<pre data-language="shell">$ docker-compose stop
Stopping myapp_web_1 ... done
Stopping myapp_db_1 ... done

# Visiting localhost:3000 will now show a connection refused error as the containers are not running.

$ docker-compose start
Starting db ... done
Starting web ... done</pre>
<p>To stop and completely remove the containers (including all their data), use the <code>docker-compose down</code> command:</p>
<pre data-language="shell">$ docker-compose down
Stopping myapp_web_1 ... done
Stopping myapp_db_1 ... done
Removing myapp_web_1 ... done
Removing myapp_db_1 ... done
Removing network myapp_default</pre>
<p>(Note that you can also stop and remove individual service containers using, for example, <code>docker-compose stop web</code> and <code>docker-compose rm web</code>).</p>

<p>Using <code>docker-compose down</code> will also remove any other unnecessary resources that were created, such as the virtual network (in this case <code>myapp_default</code>). Data volumes (which we've not used here) are not removed by default, allowing you to persist data between runs.</p>

<h2 id="use-docker-compose-in-your-apps">Use Docker Compose in your Apps</h2>

<p>Compose is a great tool, and forms the basis of more advanced Docker tools (such as configuring stacks for  Docker Swarm orchestration).</p>

<p>You can see how using Docker Compose greatly simplifies the configuration of your containers. By specifying the various services necessary for your app in a single place, it is easy to create disposable instances of your app for development, and share configuration with other developers.</p>

<p>Adding a <code>docker-compose.yml</code> file to your repository is a great way to make getting started with your app as easy as possible. Often, your <code>README</code> file for people to get started using your app can be as simple as:</p>
<pre data-language="plain">$ git clone https://github.com/your-app.git
$ cd your-app
$ docker-compose run --rm web bin/rails db:setup
$ docker-compose up -d
$ open http://localhost:3000/</pre>
<h2 id="next-steps">Next Steps</h2>

<p>Thanks for reading this tutorial. I hope you've found it useful and you can begin using Docker and Docker Compose in your own projects. Let me know how you get on by <a href="/contact/">getting in touch</a>.</p>

<p>Next time, we'll explore how you can use additional services for your Rails apps with Docker compose, such as Sidekiq and redis.</p>
]]>
      </description>
      <pubDate>Sat, 09 Sep 2017 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-on-docker-using-docker-compose-with-your-ruby-on-rails-apps</link>
      <guid>https://www.chrisblunt.com/rails-on-docker-using-docker-compose-with-your-ruby-on-rails-apps</guid>
      <category>docker</category>
      <category>docker-compose</category>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Conquering the Fear of a Fixed Price</title>
      <description>
        <![CDATA[<p><em>Fixed-Price, Fixed-Scope are words guaranteed to strike fear in software developers everywhere.</em></p>

<p>The benefits of value pricing sound obvious. No ambiguity; fair compensation; a multiple-of-X return on your customer’s investment; no surprise invoices (or subsequent “difficult conversations”), and so on. It seems like a clear win-win.</p>

<p>So why is it that when you’re ready to use value pricing and talking with a prospective customer, you suddenly get hit with the fear?</p>

<p>It usually happens just after you’ve been given a long list of requirements, and hear the words:</p>

<blockquote>
<p>So, how much will this cost…?</p>
</blockquote>

<p>Despite all the reading, all the podcasts, all the books and all the time spent convincing yourself of the benefits of value pricing, a dark cloud of doubt is suddenly cast across your mind.</p>

<p>As you try to consider your customer’s needs, and understand their goals, your head races with questions, such as:</p>

<ul>
<li>Do I know enough about this project?</li>
<li>What if they want to do X?</li>
<li>If I give a price now, what if they decide to use Y?</li>
<li>What if it takes longer than I think?</li>
<li>Is my price too high?</li>
<li>Is my price too low?</li>
<li>What if doesn’t take as along as I think? Will they want a refund?</li>
<li>What if they want to add more stuff?</li>
<li>But <em>what if</em>…?</li>
</ul>

<p>You start to second-guess yourself. Are you really ready for this value pricing? Imposter syndrome sets in, and suddenly time-based billing doesn’t seem so bad after all (<a href="https://www.chrisblunt.com/why-you-should-stop-charging-for-your-time">it is</a>). You’ll just do it one last time, but next time you’ll definitely try out value pricing…</p>

<p>The reason behind this fear, I believe, is that we often don’t accept (rather than don’t know) the value we deliver.</p>

<p>It is an indicator that there you are missing a piece of the puzzle. You haven’t yet uncovered the real value — the pain that has caused the customer to come to you in the first place.</p>

<p>It’s a sign that you need to continue the conversation.</p>

<h1 id="what-does-your-customer-value">What Does Your Customer Value?</h1>

<p>Value to your customers is likely to be very different to value to you. Perfect test coverage and a flawless <a href="https://en.wikipedia.org/wiki/Continuous_integration">CI setup</a> probably won’t interest them — unless you can demonstrate the benefit to them (e.g. “we won’t unknowingly break something when we deploy”, or perhaps “we can confidently deploy at 5pm on a Friday…”).</p>

<p>To your customer, though, perhaps there is immense value in a simple conversation; learning about how the latest release of iOS or Android will help benefit <em>their</em> customers; a video on the latest trends in their industry; implementing an off-the-shelf piece of software that will make their lives easier, and so on.</p>

<p>Even something as simple as sending an article or blog post to your customer can provide incredible value — especially if they receive it at just the right time to be relevant to their needs.</p>

<p>Take the time to have conversations with your customer, and understand the benefits they receive a result of your work. This is the best way to conquer the fear, and be confident that your proposal represents immense value to both you and your customer.</p>

<hr>

<p>Have you started using value pricing in your business? Do you get hit with the fear when asked to give a price? I’d love to hear how you’re conquering those fears and using value pricing in your business. Let me know by <a href="https://www.plymouthsoftware.com/contact">getting in touch</a>.</p>
]]>
      </description>
      <pubDate>Fri, 11 Aug 2017 12:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/conquering-the-fear-of-a-fixed-price</link>
      <guid>https://www.chrisblunt.com/conquering-the-fear-of-a-fixed-price</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Rails on Docker: Getting Started with Docker and Ruby on Rails</title>
      <description>
        <![CDATA[
  <strong>3 December 2018:</strong> Use <code>ruby:2.5-alpine</code> Docker images.


<p>Docker is a fantastic tool for isolating your app and its environment, and allows easy distribution and state-replication across multiple environments (dev, test, beta, prod, etc.). Using Docker can get rid of the "it works on my machine" problem, and help you to easily scale your app as it grows.</p>

<p>Docker is particularly great when your app has a lot of dependencies, or requires specific versions of libraries and tools to be configured.</p>

<p>In this tutorial, you'll learn how to take a basic Rails app and prepare it for use in a Docker container ("<em>dockerise</em>" it).</p>

<h2 id="prerequisites">PreRequisites</h2>

<p>For this tutorial, I'm using a simple Rails 5 application configured to use a PostgreSQL database. If you use a different database, you'll need to tweak a few of the files below.</p>

<p>You can use the following template to create a basic Rails application that is configured with a <code>Dockerfile</code> and <code>config/database.yml</code> as below:</p>
<pre data-language="shell">$ rails new --database=postgresql --skip-bundle --template=https://gist.githubusercontent.com/cblunt/1d3b0c1829875e3889d50c27eb233ebe/raw/8fce95533db3a3be19cb1aa31054589d219433c2/rails-docker-pg-template.rb my-app
$ cd my-app</pre>
<h3 id="database-configuration">Database Configuration</h3>

<p>We can make use of environment variables to configure the details for our app's database. You'll use this later so that your app's docker container can connect to a PostgreSQL container.</p>

<p><strong>Edit your <code>config/database.yml</code> configuration</strong></p>

<p><em>(Note: You don't need to do this if you've used the application template above)</em></p>

<p>Update your app's <code>config/database.yml</code> to use environment variables:</p>
<pre data-language="yaml"># config/database.yml
default: &amp;default
  adapter: postgresql
  encoding: unicode
  pool: &lt;%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %&gt;
  host: db
  username: &lt;%= ENV.fetch('POSTGRES_USER') %&gt;
  password: &lt;%= ENV.fetch('POSTGRES_PASSWORD') %&gt;

development:
  &lt;&lt;: *default
  database: my-app_development

test:
  &lt;&lt;: *default
  database: my-app_test

production:
  &lt;&lt;: *default
  database: my-app_production</pre>
<h2 id="create-a-dockerfile">Create a Dockerfile</h2>

<p>With your app prepared, it's time to start using Docker. Let's start by creating a <code>Dockerfile</code>. This is a plain text file that instructs Docker how to build an image for your application.</p>

<p>You use it to install dependencies, configure default environment variables, copy code into the container, and so on.</p>

<p>To keep your image size small, I prefer to use the alpine-linux Ruby base image. Alpine linux is a tiny linux distribution that's perfect for containers, and Docker provides a default <code>ruby:alpine</code> base image that we can use.</p>

<p><strong>Write Your <code>Dockerfile</code></strong></p>

<p>Let's start by creating a basic <code>Dockerfile</code> for your rails app. In your app's folder, create the following <code>Dockerfile</code>.</p>

<p><em>(Note: You don't need to do this if you've used the application template above)</em></p>



<hr>

<p><strong>What if I don't use PostgreSQL?</strong></p>

<p>If you're using a different database server (e.g. MySQL), you'll need to tweak the Dockerfile to install the appropriate packages.</p>

<p>You can search for the correct package(s) using the following Docker command:</p>
<pre data-language="shell">$ docker run --rm -it ruby:2.5-alpine /bin/sh -c 'apk update &amp;&amp; apk search mariadb | sort'
...
mariadb-client-10.2.15-r0
mariadb-common-10.2.15-r0
mariadb-connector-c-3.0.4-r1
mariadb-connector-c-dev-3.0.4-r1
mariadb-dev-10.2.15-r0
...
mysql-10.2.15-r0
mysql-bench-10.2.15-r0
mysql-client-10.2.15-r0
...
</pre>
<hr>

<p>With your <code>Dockerfile</code> written, you can now instruct Docker to build an image for your app:</p>

<p><strong>Build an image for your app</strong></p>
<pre data-language="shell">$ docker build . -t my-app</pre>
<p>Once the image is built, we're ready to get started! You can spin up a new container based on your app's image using the following command:</p>
<pre data-language="shell">$ docker run --rm -it --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --publish 3000:3000 --volume ${PWD}:/app my-app</pre>
<p>We're passing a few arguments to the Docker run command:</p>

<ul>
<li>
<code>-it</code> is actually two arguments that allow you to interact with your container via the shell (e.g. to issue Ctrl+C commands).</li>
<li>
<code>--env</code> allows you to pass environment variables into the container. Here, you're using it to set the database connection values.</li>
<li>
<code>--rm</code> will instruct Docker to remove the container once it finishes (i.e. when you hit Ctrl+C).</li>
<li>
<code>--publish</code> forwards port 3000 from the container to port 3000 on your host. This allows you to access the container as if it was running on your host (i.e. <code>http://localhost:3000</code>).</li>
<li>Finally <code>--volume</code> instructs Docker to "mount"  the current folder (from your host machine) into the container. This means as you edit code on your workstation, it will be available to the container. Without this, you'd need to recreate the container with every code change you make.</li>
</ul>

<p>Once it's up and running, you can access your container by opening <a href="http://localhost:3000">localhost:3000</a> in your browser.</p>

<h2 id="run-a-database-container">Run a Database Container</h2>

<p>Unfortunately, although the container runs successfully, if you try to access the app in your browser, it will crash with a database error.</p>

<p><em>could not translate host name "db" to address: Name does not resolve</em></p>

<p>At the moment, there isn't a PostgreSQL server available for the app too connect. We'll fix that now by spinning up a separate Docker container in which PostgreSQL will run:</p>

<hr>

<p><strong>ProTip</strong> Remember that in Docker, a container should be designed to do one thing (and one thing only).</p>

<p>In our case, we'll use two containers: one for our app, and one for our database (PostgreSQL).</p>

<hr>

<p><strong>Start a new container running PostgreSQL:</strong></p>

<p>Hit Ctrl+C to stop (and remove) your running app container, then spin up a new container for PostgreSQL:</p>
<pre data-language="shell">$ docker run -d -it --env POSTGRES_PASSWORD=superSecret123 --env DB_NAME=my-app_development --name mydbcontainer postgres:9.6</pre>
<p>The <code>-d</code> flag will detach the container from our terminal, allowing it to run in the background. We also give the container  a name (<code>mydbcontainer</code>) which we'll use below.</p>

<h3 id="using-single-task-containers">Using Single–Task Containers</h3>

<p>Docker containers are disposable, and their single-purpose nature means that once they have "finished", they are stopped (and, optionally, removed).</p>

<p>This makes them perfect for running one-off tasks, such as rails commands (e.g. <code>bin/rails db:setup</code>).</p>

<p>We'll do that now to setup your app's database on <code>mydbcontainer</code>:</p>

<p><strong>Run the <code>rails db:migrate</code> task using a container:</strong></p>

<p>Use the following command to spin up a copy of your app's container, run the <code>bin/rails db:setup</code> task, and then shut down.</p>

<p>Note that you'll need to configure environment variables for the database connection (these are injected into the <code>config/database.yml</code> file you edited earlier).</p>

<p>You'll also use the <code>--link</code> option which allows the container to connect to PostgreSQL container that is running (<code>mydbcontainer</code>), using the hostname <code>db</code>:</p>
<pre data-language="shell">$ docker run --rm --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --link mydbcontainer:db --volume ${PWD}:/app my-app bin/rails db:create db:migrate</pre>
<p>The <code>--rm</code> flag will remove (delete) the container once it has finished running.</p>

<p>Once that command has run, your app's database will be setup on the <code>mydbcontainer</code> container. Finally, we can run our app!</p>

<p><strong>Run your app!</strong></p>

<p>Let's spin up a new container using our app's image. Notice that there are a couple of additional arguments when running the command:</p>
<pre data-language="shell">$ docker run --rm -it --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --publish 3000:3000 --volume ${PWD}:/app --link mydbcontainer:db my-app

=&gt; Puma starting in single mode...
=&gt;  * Version 3.8.2 (ruby 2.4.1-p111), codename: Sassy Salamander
=&gt;  * Min threads: 5, max threads: 5
=&gt;  * Environment: development
=&gt;  * Listening on tcp://0.0.0.0:3000
=&gt;  Use Ctrl-C to stop</pre>
<p>Open your browser to <a href="http://localhost:3000">localhost:3000</a>, and you should see your app running entirely on Docker!</p>

<h2 id="next-steps">Next Steps</h2>

<p>Docker is a great tool for developing your applications. As time goes on, you can begin to move all components of your app (database, redis, sidekiq workers, cron, etc.) to Docker.</p>

<p>The next step is to use <a href="https://docs.docker.com/compose/overview/">Docker Compose</a> to declare all your containers, and how they should work together.</p>
]]>
      </description>
      <pubDate>Fri, 07 Jul 2017 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-on-docker-getting-started-docker-ruby-rails</link>
      <guid>https://www.chrisblunt.com/rails-on-docker-getting-started-docker-ruby-rails</guid>
      <category>docker</category>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>3 Steps to Start Value Pricing</title>
      <description>
        <![CDATA[<p><em>Changing from time billing to value pricing is an ongoing journey. More than anything else, value pricing forces change in your own mind about the work you do, and how you help your customers to reach their goals.</em></p>

<p>It can be difficult to get started with value pricing, though. It can seem a daunting, and risky, move. I spent months trying to reconcile the “fixed-price, fixed-scope” nature of value pricing with the fluidity of software projects, constantly worrying about how to deal with scope-creep.</p>

<p><em>This is the second in a series of articles based on my talk at FutureSync held at <a href="https://www.plymouth.ac.uk/">Plymouth University</a> on 9th June 2017. In Part 1, I discussed why charging for time is against the interests of both you and your customers.</em></p>

<p>Eventually, I figured the only way was to jump in 100%: force myself to abandon time-billing altogether, and accept that I will make many mistakes along the way…</p>

<p>As with most things, the hardest step was actually getting started. Once I made the leap, things began to change for the better. In fact, the journey has led to major changes in all areas of my business.</p>

<p>In this post, I’ll introduce three steps you can take to start value pricing in your business. These steps help to change your thinking away from the time you put in, towards the value and results your customers get out.</p>

<h1 id="1-increase-your-time-billing-unit">1. Increase Your Time-Billing Unit</h1>

<p>Instant, easy and effective. Increasing the time unit for which you charge has an immediate effect on your business’ top line.</p>

<p>If you are currently charging for hours, start charging for days; if you’re charging for days, start charging for weeks; and if you’re already charging by the week, start charging for 2-weekly sprints, or — even better — monthly.</p>

<p>Of course, this isn’t really value pricing — you are still charging based on your time. However, you’ll very quickly find your customer interactions change. As you move up the time-billing units scale, and particularly once you are charging on a weekly basis, the conversations you have with your customers change dramatically.</p>

<p>As small time increments play less of a part in your work (15 minutes here, an hour or two there), and especially once you’re billing by the week, you should find customers become more concerned with the results of your time, rather than the time-sheets you produce, and in turn you can begin to work with them on the bigger-picture goals of their project.</p>

<h2 id="protip-never-use-fractional-units">ProTip: Never use fractional units</h2>

<p>The key to this step is not to fall back to your previous time unit. If your new billing unit is a day, that becomes your new minimum fee. No half days (or quarter days, or 0.1333 days…).</p>

<p>I made this mistake in the early days, and it meant the conversations I was having with customers never changed — after all, if you’re just changing the label from hours to days, nothing changes in the eyes of the customer.</p>

<p>Some customers will understandably resist when, for example, your price rises from a couple of hours to a “day” for what they consider to be small task.</p>

<p>Remember that your goal is to price your work based on the value your customer receives, rather than your efforts going in. One way to help reduce sticker-shock for your customer is to simply drop the “days” label, and offering the work as fixed price service instead for a set of defined outcomes instead. More on this below.</p>

<p>Needless to say, the quicker you can move away from labelling your work with time (hours, days, weeks, etc.), the better. That’s where the next steps come in.</p>

<h1 id="2-have-conversations">2. Have Conversations</h1>

<p>This is where you begin to discover the value you’re really delivering to our customers. Conversations are critical to the success of your business, and your relationships with your customers.</p>

<p>Now, it’s important to clarify that by conversations, I don’t mean sales meetings, pitches, or job interview-esque discussions (those one-way conversations where you sit with a customer, they dictate a bunch of tasks, and you head off to get started)</p>

<p>Value-driven conversations require really getting to know your customer’s business, understanding their long-term goals, and building huge amounts of trust between you.</p>

<blockquote>
<p>Trust is the key that lies behind every successful project, and what you will need in order to make the shift to value pricing.</p>
</blockquote>

<p>As you learn more about your customer’s business, you’ll naturally begin to see new opportunities for them.</p>

<p>Remember that you are likely far more aware with what’s going on in your industry than your customer, and so being able to offer them advice and knowledge about what’s going on — and, more importantly, how it can benefit them — is incredibly valuable.</p>

<p>The goal is to become a trusted advisor to your customer and their business; someone they can rely on to act in the best interests of them and their business.</p>

<p>This requires regular engagement, conversations about more than just the project; and also that you become familiar with their business and industry.</p>

<h2 id="protip-say-no">ProTip: Say No</h2>

<p>A quick way to gain trust is to start with no. Say no to those off-the-cuff ideas that tend to crop up in conversation, and that you know will deliver little or no benefit to your customer.</p>

<p>We all succumb to shiny-new-thing syndrome once in a while, and your customer is no different. Whenever they ask for something out of the blue (e.g. <em>“we just need to implement live chat on our website.”</em>), force them to justify the business case behind it, and how that will help them work towards their goals.</p>

<p>If your customer can’t justify the feature or request yet, but still feels strongly about it, then suggest putting it on an backlog or wish-list for future consideration, and set a date a few weeks out to review it.</p>

<p>What seems critical and urgent today can and will change as things progress and new opportunities arise.</p>

<h1 id="3-productise">3. Productise</h1>

<p>As you move up the scale of time-billing units, the amount of time you put into your work becomes less relevant (it doesn’t necessarily decrease though).</p>

<p>Through the conversations you’re having with your customers, you’ll begin to see common patterns, needs and requirements. As you do, you can begin to extract the work you do to satisfy those requirements and bundle them into products, or <a href="https://casjam.com/productize">productised services</a>.</p>

<p>Doing this allows you to establish standard procedures for certain functions of your business which you can continuously work to make more efficient, review and test with your customers.</p>

<blockquote>
<p>One of the greatest benefits to you and your customer of a product(-ised service) is that you can set and advertise it with a price.</p>
</blockquote>

<p>Now you can begin to remove reference to your time from the service, and just focus on outcomes.</p>

<p>Of course, you will want to consider your own time as a cost of business — especially if you have a team of employees or contractors.</p>

<p>As far as the customer is concerned, though, they are getting known outcomes for a specified price. They can determine themselves if the value is right for them, greatly speeding up your qualification and sales process.</p>

<h2 id="protip-give-your-customer-options">ProTip: Give Your Customer Options</h2>

<p>Options are beneficial to any pricing strategy, but especially helpful in value pricing. Presented with options rather than a binary yes/no proposal, your customer can determine their own level of return on investment based on the options presented.</p>

<p>Three is the popular number for presenting options, but even a simple A/B choice is better than a single offer, so find a way to increase the value between two options, and make sure you highlight them to your customer.</p>

<p>The higher price options would usually build upon earlier options, increasing the value exponentially as you move up the scale, but this can be dependent on the nature of the project.</p>

<h1 id="4-over-deliver">4. Over-Deliver</h1>

<p><em>(pun intended… :)</em></p>

<p>Over deliver on absolutely everything you do for your customer.</p>

<p>It’s important to point out that I don’t mean using the tired “under-promise, over-deliver” sales trick (that’s <a href="https://blog.hubspot.com/sales/underpromise-and-overdeliver-is-terrible-sales-advice">terrible sales advice</a>). Remember, the key to success with value pricing is trust.</p>

<p>You can only really start doing this once you’ve successfully moved your own mind away from time-billing; appreciate the value you are delivering to your customers, and — most importantly — are pricing based on that value.</p>

<p>However, over-delivery comes naturally result of a value-based mindset. You can’t help but want to find new ways to help your customer achieve their goals, because your focus will be on maximising the impact of your work on their business, knowing that you are both getting value out of the transaction.</p>

<blockquote>
<p>True value pricing is always a win-win.</p>
</blockquote>

<hr>

<p>I hope you find these techniques helpful, and can use them in your own business. I’d love to know and hear how you’re getting on — <a href="https://www.chrisblunt.com/contact/">get in touch</a>, <del>or reach me on twitter</del>.</p>
]]>
      </description>
      <pubDate>Mon, 03 Jul 2017 12:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/3-steps-to-start-value-pricing</link>
      <guid>https://www.chrisblunt.com/3-steps-to-start-value-pricing</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Why You Should Stop Charging for Your Time</title>
      <description>
        <![CDATA[<p><em>Time-based billing is how most of us start our freelance and consulting careers, but it quickly becomes obvious that it causes many problems. I set out to find a better way..</em></p>

<p><em>This is Part 1 in a series of articles based on a talk I gave at the FutureSync Conference held at <a href="https://www.plymouth.ac.uk/">Plymouth University</a> on 9th June 2017.</em></p>

<h1 id="the-problem-with-time">The Problem with Time</h1>

<p>As I imagine is the usual story, when I set up <a href="https://www.plymouthsoftware.com/">Plymouth Software</a> (and indeed, when I was freelancing before), charging by the hour was the norm. After all, it’s just “the way things are done”.</p>

<p>The story goes something like this: Once you’ve decided to make the plunge and set up your own business, you head to one of the <a href="https://www.google.co.uk/search?q=freelance+rate+calculator">many</a> freelance rate calculators. You tap in your living costs; some estimated budgets for nice-to-haves (coffees, holidays, new laptops, etc.); your expected number of holiday days, and a target profit margin. With that all done, you hit submit and out pops your new hourly rate. You round the figure, and set off to work.</p>

<p>Pretty quickly, though, you learn that selling your time is really quite painful. And it’s not just painful for you — it’s just as bad for your customers.</p>

<p>Understandably, they don’t want any ambiguity in what they are going to pay you, and they want to know what they’re going to get for their money. We’ve all been on the other side of those transactions, and nobody likes receiving those surprise end-of-the-month invoices.</p>

<p>It’s a hard lesson, but you quickly come to realise that time-based billing—a relic of the industrial age—simply isn’t fit for purpose in the world of knowledge-based and professional service businesses.</p>

<p>It is fraught with problems and frustrations for everyone involved, but there are three major problems with time-billing that underpin all the others.</p>

<h1 id="1-time-billing-puts-you-in-a-position-of-conflict-with-your-cust">1. Time billing puts you in a position of conflict with your customer from day one.</h1>

<p>This is perhaps the most fundamental issue with time-based billing — every project becomes a zero-sum game.</p>

<p>As a business, you want to maximise income, so you need to work as many hours onto the timesheet as possible. Naturally, your customer wants to minimise their costs, and so they want to keep those hours down. In this scenario, a win-win scenario is impossible.</p>

<p>Recognising this, we usually try to mitigate the damage by employing fixed-price quotes.</p>

<p>Now, you estimate how long the job will take, and quote a price for the customer. They get some certainty in their spend, but you have taken on virtually all of the risk.</p>

<p>Unfortunately, this type of fixed-price quote doesn’t solve the problem — it simply reverses the dynamic. You want to minimise hours to maximise your effective hourly rate, but your customer wants to maximise their return, squeezing as much as possible from that quote and reducing your effective hourly rate.</p>

<p>This is where (in software development, at least) we fall into the trap of hefty upfront specification documents, and pretending_ to know how things will be three months down the line.</p>

<p>In all but the rarest cases, fixed-price quotes based on time usually result in big squeeze of work near the end, as that estimated number-of-hours draws close, and work is crammed in to maximise the budget.</p>

<p>Unfortunately, the end result is usually poor relationships and awkward conversations about what was included in the original quote, where things went wrong, the finer details of the contract, etc.</p>

<p>This is valuable time and energy that would be much better spent delivering results for your customers.</p>

<h1 id="2-time-billing-encourages-busy-work">2. Time billing encourages busy work</h1>

<p>Time-based billing naturally encourages doing something rather than the <em>right</em> thing. This often means we jump into a project without really knowing the end goal. We skip learning what success looks like for our client to get hours on the timesheet.</p>

<p>This almost inevitably leads to disappointed customers. Without taking the time to really understand your customer’s needs and goals, we end up delivering nothing of value.</p>

<p>For example, if a customer comes to you saying they need a new website, the last thing you should do is start considering which of the latest cool new frameworks you want to use.</p>

<p>Instead, you should start questioning why they think they need a website — and keep working with them until you reveal the underlying goals behind their request.</p>

<blockquote>
<p>Nobody wakes up one morning thinking “I’ll spend a few thousand on a website today.”</p>

<p>There is always an underlying reason.</p>
</blockquote>

<p>The problem with time billing is it encourages us to skip the learning phase, and <em>deliver</em> the website.</p>

<p>A few weeks later, our customer has a shiny new website, a reduced bank balance, and the same problems as before.</p>

<p>We’ve delivered what the customer asked, but failed to solve the issue that led to them asking us in the first place.</p>

<h1 id="3-time-billing-puts-a-ceiling-on-your-income">3. Time Billing puts a ceiling on your income</h1>

<p>Admittedly, this is not a problem with which your customer is concerned, but as a business-owner it should worry you. There are only so many hours in a day, and so by billing for your time, you are putting an artificial ceiling on your potential income.</p>

<p>There are 3 main ways we can try to overcome this issue:</p>

<h2 id="i-increase-your-workload">i) Increase your workload</h2>

<p>This is OK in the short-term, but eventually you’ll burn out or hit the very hard limit of 24 hours a day.</p>

<h2 id="ii-increase-your-rate">ii) Increase your rate</h2>

<p>Certainly this will be the quickest and easiest way to break through the income ceiling. However, your rate is also subject to the market, and the laws of economics mean there will be an increasing downward pressure on your rate. This is especially true as implementation work becomes more commoditised. Ultimately, you’ll hit another ceiling.</p>

<h2 id="iii-increase-your-staff">iii) Increase your staff</h2>

<p>Likely the most effective strategy over the long-term, but taking on staff may require big changes to your business — particularity if you’re currently solo.</p>

<p>Unfortunately, though, increasing staff just scales up the problem of time billing: now you’re selling ten people’s time rather than just one or two—and you’re still subject to the same problems as before.</p>

<h1 id="finding-a-better-way">Finding a Better Way</h1>

<p>I knew there had to be a better way than time billing, and set out to find it. Fairly early on, I learned the concept of Value Pricing from the first edition of <a href="https://doubleyourfreelancing.com/rate/">Brennan Dunn’s Double Your Freelancing Rate</a> (a book I highly recommend)</p>

<p>As I read more into the subject, I became increasingly convinced value pricing was the only way to truly escape time-based billing, and grow my business.</p>

<p>There was one problem though.</p>

<p>I struggled a lot to reconcile how value pricing could work with software development. There are just so many unknowns in software projects…but it turns out this we’re not alone in this!</p>

<blockquote>
<p>Changing your own mindset is perhaps the single biggest challenge of switching to value pricing.</p>
</blockquote>

<p>After all, I’d spent 30-odd years being taught that <em>time is money</em> and everything in business is reduced down to an hourly rate. The modern world of work — especially in service industries — is still very much designed around the billable hour.</p>

<p>By doing anything different, you’re going up against a lot of expected norms.</p>

<p>In 2016, I decided the only way I could make value pricing succeed was to <a href="https://www.chrisblunt.com/hello-brave-new-world">embrace it 100%</a>, force myself to abandon time billing, and accept that mistakes will be made…</p>

<hr>

<p><em>In the next part of this article series, I’ll talk more about what value pricing is, and outline 3 practical steps you can use right away to begin changing your business to value pricing.</em></p>
]]>
      </description>
      <pubDate>Tue, 20 Jun 2017 12:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/why-you-should-stop-charging-for-your-time</link>
      <guid>https://www.chrisblunt.com/why-you-should-stop-charging-for-your-time</guid>
      <category>business</category>
      <category>value-pricing</category>
    </item>
    <item>
      <title>How to Fix Missing SD Card on Android Emulator</title>
      <description>
        <![CDATA[<p>Whilst testing an app on the Jelly Bean emulator, I couldn't get the emulator
to mount the SD card. Calls to <code>Environment.getExternalStorageState()</code> would
always return <code>Environment.MEDIA_REMOVED</code>.</p>

<p>After some searching, I realised the found was being configured with
<code>hw.sdCard=no</code> during creation, despite specifying an SD card in the UI.</p>

<p>To fix the issue, open up the <code>config.ini</code> for the affected emulator, and
change the value to <code>hw.sdCard=yes</code>, e.g:</p>
<pre data-language="ini"># ~/.android/avd/Nexus_5X_API_16.avd/config.ini
# ...
hw.sdCard=yes</pre>
<p>After restart your emulator, it should now successfully mount the SD Card.</p>
]]>
      </description>
      <pubDate>Fri, 19 May 2017 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/fix-missing-sdcard-android-emulator</link>
      <guid>https://www.chrisblunt.com/fix-missing-sdcard-android-emulator</guid>
      <category>android</category>
      <category>emulator</category>
      <category>tips</category>
    </item>
    <item>
      <title>How to Pass Data From Rails to Javascript / Coffeescript</title>
      <description>
        <![CDATA[<p>If you want to pass data between Rails and Coffeescript in your apps, you'll
find that interpolation doesn't work. For example:</p>
<pre data-language="coffeescript"># app/assets/javascript/home.coffee
$ -&gt;
  $('#some_element' ).text('&lt;%= @user.id %&gt;')</pre>
<p>If you look at the resulting Javascript, you'll see that the string is
interpreted literally, rather than interpolating the user's <code>id</code>:</p>
<pre data-language="javascript">(function() {
  $(function() {
    $('#some_element' ).text('&lt;%= @user.id %&gt;');
  });
}).call(this);</pre>
<h2 id="quickfix">Quickfix</h2>

<p>You can quickly fix this by appending <code>.erb</code> to the Coffeescript's filename.
This will instruct the asset compiler to parse the file through ERB before
Coffeescript, resulting in the value being correctly interpolated:</p>
<pre data-language="coffeescript"># app/assets/javascript/home.coffee.erb
$ -&gt;
  $('#some_element' ).text('&lt;%= @user.id %&gt;')</pre><pre data-language="javascript">// (resulting Javascript)
(function() {
  $(function() {
    $('#some_element' ).text('32');
  });
}).call(this);</pre>
<h2 id="a-more-flexible-approach">A More Flexible Approach</h2>

<p>However, I feel this too tightly-couples the Javascript and Rails
code. I much prefer to follow Rails' own unobtrusive-Javascript approach, and
make use of HTML5 data attributes to pass data between the front-end (JS) and
back-end (Rails) code.</p>

<p>Using the example above, the <code>id</code> would be passed as an arbitrary data attribute
on the HTML element:</p>
<pre data-language="erb"># app/views/home/index.html.erb
&lt;div class="user-details" data-user-id="&lt;%= @user.id %&gt;"&gt;
&lt;/div&gt;</pre>
<p>Now in the Javascript, you can easily extract the value of <code>data-user-id</code>.
Furthermore, you can use the same code to act on any HTML element containing the
data attribute:</p>
<pre data-language="coffeescript"># app/assets/javascript/home.coffee
$ -&gt;
  $('div[data-user-id]' ).each -&gt;
    userId = $(this).data('user-id')
    $(this).text(userId)</pre>
<p><small class="i">This post was inspired by a question on StackOverflow. <a href="https://stackoverflow.com/a/43828836/147180">See the original
discussion</a>.</small></p>
]]>
      </description>
      <pubDate>Sun, 07 May 2017 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/how-to-pass-data-from-rails-to-javascript-coffeescript</link>
      <guid>https://www.chrisblunt.com/how-to-pass-data-from-rails-to-javascript-coffeescript</guid>
      <category>coffeescript</category>
      <category>javascript</category>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>3 Ways to Keep Your Ruby on Rails Apps Healthy</title>
      <description>
        <![CDATA[<p>Ruby on Rails is a great framework for building modern web applications. But as with any technology, things move on quickly. Without proper care and attention, the web apps and sites you build using Rails can start to degrade. This means slow pages, security vulnerabilities and your users seeing error screens.</p>

<p>As building and supporting Rails apps is a core part of <a href="https://www.plymouthsoftware.com/">my business</a>, I've adopted a few tools and techniques to ensure that the apps I build and manage are easy to maintain and improve.</p>

<p>Here are 3 things you can do today to keep your Ruby on Rails apps in great condition:</p>

<h2 id="1-check-for-code-smells">1. Check for Code Smells</h2>

<p>A <a href="https://en.wikipedia.org/wiki/Code_smell">code smell</a> is a section of code in your app that indicates a deeper underlying problem. The problem might be a bug, or a something that has the potential to lead to bugs, such as duplicated blocks of code, or several levels of nested conditions.</p>

<p>Code smells are dangerous for your app because as the amount of code increases, so to does the risk of errors creeping into the code. Catching code smells early and often can help to reduce this risk, and make your code much easier for you (and other developers) to maintain in the future.</p>

<p><a href="https://github.com/whitesmith/rubycritic">RubyCritic</a> is a great tool for identifying code smells and bad practice. You can install it easily by adding the following to your app's <code>Gemfile</code>:</p>
<pre data-language="ruby"># Gemfile
gem 'rubycritic', require: false, groups: [:development, :test]</pre>
<p>Once that's done, just <code>bundle install</code> and run <code>rubycritic</code> against your app's code:</p>
<pre data-language="shell">$ cd /path/to/your/app
$ bundle install
$ bundle exec rubycritic --format html</pre>
<p>After a while, RubyCritic will generate a report on your app's code quality. If the report doesn't open automatically, you'll find it in your app's <em>tmp/rubycritic</em> folder:</p>
<pre data-language="shell">$ open tmp/rubycritic/overview.html</pre>
<p>The reports show code smells that Rubycritic has identified. Use the reports as a guide to improve your app's codebase.</p>

<h2 id="2-scan-for-security-holes">2. Scan for security holes</h2>

<p>Rails does a lot to protect you from exposing your app to security holes. But like any software, new vulnerabilities are discovered all the time. Keeping your app's version of rails up to date with a supported release is critical to protecting your app and your users.</p>

<p>Similar to Rubycritic for codesmells, we can use tools like <a href="https://github.com/presidentbeef/brakeman">brakeman</a>, you can easily scan your app's codebase for security vulnerabilities and known issues. To get started with brakeman, install it as a gem:</p>
<pre data-language="shell">$ gem install brakeman</pre>
<p>Then jump into your app's code and run the <code>brakeman</code> command:</p>
<pre data-language="shell">$ cd /path/to/your/app
$ brakeman</pre>
<p>Brakeman will then report on various levels of issues in your code, allowing you to improve the security of your app instantly.</p>

<h2 id="3-set-up-monitoring">3. Set up Monitoring</h2>

<p>Finally, once your app is out in the wild, you'll want to know if anything goes wrong. Errors can occur anywhere - and you definitely don't want them to be silent (to you).</p>

<p><img src="/assets/images/2017/statuscake-dashboard.png" alt="StatusCake Dashboard">
</p><p class="tc i">StatusCake's monitoring dashboard.</p><p></p>

<p>There are <em>lots</em> of different monitoring services out there - with a range of prices - that will help you monitor your app. Several of them, such as <a href="https://www.statuscake.com">StatusCake</a>, <a href="https://www.sentry.com">Sentry</a> and <a href="https://rollbar.com">Rollbar</a> offer free plans to get you started.</p>

<p>There are also several types of monitoring, from availability checks (as provided by <a href="https://www.statuscake.com">StatusCake</a> and <a href="https://www.pingdom.com/">Pingdom</a>) through error reporting (Sentry and Rollbar) and performance monitoring (<a href="https://www.skylight.io">Skylight</a> and <a href="https://newrelic.com/application-monitoring">New Relic</a>).</p>

<p>To get started, you can start with simple availability and error checks, as they're going to give you the instant coverage that your app needs. Most of the tools are easy to setup, either through registering a URL or dropping a gem and API key into your app's code.</p>

<p>As your user–base and site traffic increases, you should look at performance monitoring tools to keep an eye on how well your app is responding to customers, and highlight any bottlenecks or opportunities for caching.</p>

<h2 id="next-steps">Next Steps</h2>

<p>Keeping a production app in top condition is an ongoing task. To find out more about keeping your app's healthy, and how to make the process easier with tools and systems, check out my new courses at <a href="https://healthyrailsapps.com">healthyrailsapps.com</a>.</p>
]]>
      </description>
      <pubDate>Thu, 27 Apr 2017 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/3-ways-to-keep-your-ruby-on-rails-apps-healthy</link>
      <guid>https://www.chrisblunt.com/3-ways-to-keep-your-ruby-on-rails-apps-healthy</guid>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>A Year of Value Pricing</title>
      <description>
        <![CDATA[<p><em>Wow! It’s been nearly a year <a href="https://www.chrisblunt.com/hello-brave-new-world">since I made the commitment</a> to switching my business to value pricing. I’ve not updated you in a while, so I wanted to write a summary of what’s happened 12 months later…</em></p>

<p>The good news is that the switch to value pricing has, on the whole, been very successful.</p>

<p><strong>The move to a value pricing model has fundamentally changed my approach to virtually everything I do, and is even driving a change to the business itself.</strong></p>

<p>As I <a href="https://www.chrisblunt.com/scheduling-and-capacity-planning-with-value-pricing">discussed in my previous post</a>, when you start proposing work based on value, it forces you to begin thinking about what it is you’re delivering (lesson learned: it’s not code, designs or apps).</p>

<p>Here are some of the changed I made and benefits I’ve experienced from moving to value pricing.</p>

<h1 id="breaking-the-time-billing-mindset">Breaking the Time Billing Mindset</h1>

<p>One of the biggest challenges is overcoming the general expectation for time-based billing, particularly in service-based businesses. This has been as much my own expectation as anyone else’s.</p>

<p>My commitment was to move the business entirely to value pricing, and I’m pleased to say that this has been the case in the vast majority of jobs I’ve taken on. I’ll confess, though, there have been a couple of occasions when I’ve charged for time for very small development work (minor fixes, tweaks and so on) where the “value” was just getting the job done.</p>

<p>I still wrapped these in fixed-fee quotes, but they were very much what Ron Baker describes as <em>Hourly Billing in Drag</em>. I ensured these jobs were small though; nothing more than a day or two.</p>

<p>However, I knew my mindset had changed when I experienced another unexpected outcome of the switch: I now expect other companies to offer me value-based prices and options, rather than giving me a time estimate or rate.</p>

<p>In fact, I’ve found myself specifically opting for services that are priced in this way (even if the price may seem higher than a time-estimate) because I’m aware of the advantages of value-pricing to me as a customer.</p>

<h1 id="proposals">Proposals</h1>

<p>Without doubt, one of the quickest benefits I saw from the switch to Value Pricing has been in writing proposals. They are now considerably faster to put together, as generally they are more a summary of what has gone before them, rather than the “send-and-hope” pitches they were before.</p>

<p>I changed my proposals from a report style to a personalised letter format. As in any business, my goal is to build lasting business relationships, so this personal touch helps in fostering a more collaborative relationship.</p>

<p>The letter format also allows me to cover various things in a far more conversational manner — which is far easier to write! — and reflect on discussions that have happened in the stages before the proposal.</p>

<h1 id="customer-satisfaction">Customer Satisfaction</h1>

<p>As you would expect, customers are <em>far</em> happier when they know what they are getting and how much it will cost, rather than an estimate of hours, days and weeks.</p>

<p>Coupled with additional benefits that can be offered within a price, such as providing a satisfaction guarantee, customers are usually very happy (if not a little surprised) to learn that I offer prices over rates.</p>

<h1 id="productisation">Productisation</h1>

<p>My move to productisation started with introducing <em>Discovery Workshops</em> for new projects.</p>

<p>Rather than the usual “email, chat, coffee, proposal, cross-fingers” cycle, I recognised that those initial discussions with potential customers can deliver incredible value — even if it ultimately means the project doesn’t go ahead.</p>

<p>There’s a <em>lot</em> of value in saving customers £000’s in development costs with some early validation using the Discovery Workshop approach.</p>

<p>Productising these early stages of a project also helped me to start defining a sales process for incoming leads. I eventually offered 3 levels of discovery product— a short consultation, a full-day workshop, and a workshop plus wireframes/working mockups. This gave new customers 3 levels of investment to choose from, as well as helping me to to qualify leads.</p>

<p>Although I only ran a few of the workshops last year, I learned a lot from each one and got positive feedback from customers. Not all customers opted for Discovery (sometimes they wanted to jump right into building a working prototype), those that did got real value from the sessions, as well as something they could take away and use to further develop, refine or invest in their idea.</p>

<h1 id="development-sprints">Development Sprints</h1>

<p>I’ve used the Sprints straight out of SCRUM as part of my agile process for several years, but I’ve always struggled to move the charging structure away from time.</p>

<p>Previously, I would pitch a Sprint as 1 or 2 weeks worth of work. In a (admittedly basic) attempt to price them by value, I removed “weeks” from the equation.</p>

<p>This worked, but it’s not even close to true value pricing — each Sprint could arguably deliver a very different level of value from the next. However, in the early stages of a new software project, it is extremely difficult to determine any value beyond “getting it built”.</p>

<p>Fixed-price, fixed-scope Sprints offered a quick-fix, but they weren’t perfect. However you frame it, development work is difficult to value price because the end-result is bespoke.</p>

<p>So whilst Sprints provided a way to move my charges away from time, they still weren’t a good for value pricing.</p>

<p>As time went on, I came to realise that development work is so difficult to value price because it’s selling the wrong thing.</p>

<h1 id="real-value">Real Value</h1>

<p>The truism goes that::</p>

<blockquote>
<p><a href="https://en.wikiquote.org/wiki/Theodore_Levitt">People don’t want to buy a quarter-inch drill, they want a quarter-inch hole.</a></p>
</blockquote>

<p>I recently listened to an <a href="https://doubleyourfreelancing.com/business-freelancing-episode-52-eric-white-jobs-done/">excellent interview with Eric White</a> where he took this analogy one step further: customer’s don’t want to buy the <em>hole</em> — they want the <em>picture hanging on the wall</em>.</p>

<p>The big realisation after nearly a year of value pricing my work is that the implementation (code in my case, but this could be anything) is of little — if any — real value to customers. Trying to sell development is like trying to sell the drill instead of the hole (or the picture).</p>

<p>It’s this that has got me thinking and driven me to start making some big changes to <a href="https://www.plymouthsoftware.com/">my business</a>.</p>

<p>As time goes on, I’ll cover those changes in more detail, but the first step is to shift my focus to providing Service Plans rather than new-build software projects.</p>

<h1 id="service-plans">Service Plans</h1>

<p><a href="https://www.plymouthsoftware.com/rails-service-plans">Service Plans</a> are now my main productised service offering, providing a known scope of work for a known monthly price.</p>

<p>I have purposely left time restrictions out of the Service Plans, and instead base their deliverables on scope of what’s included. As well as standard maintenance and monitoring, this approach has allowed me to include more much more valuable benefits such as consultation and (software) strategy services for customers, which are more suited to value pricing.</p>

<p>Service Plans also represent a shift away from delivering code, and instead concentrate on delivering outcomes. You can find out more about what’s included in the Service Plans at <a href="https://www.plymouthsoftware.com/rails-service-plans">plymouthsoftware.com</a>.</p>

<p>Service Plans represent the first of a number of productised services (and products) that I will offer through <a href="https://www.plymouthsoftware.com/">Plymouth Software</a>. Subscribe to get email updates as I introduce new products and services, and continue my journey to value pricing.</p>

<hr>

<p><em>Have you made the switch to value pricing in your software business? How has it changed your business?
Let me know by <a href="https://www.chrisblunt.com/contact/">getting in touch</a> <del>or message me on Twitter</del>.</em></p>
]]>
      </description>
      <pubDate>Wed, 25 Jan 2017 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/a-year-of-value-pricing</link>
      <guid>https://www.chrisblunt.com/a-year-of-value-pricing</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Scheduling and Capacity Planning with Value Pricing</title>
      <description>
        <![CDATA[<p><em>One of the biggest challenges I’ve found with value pricing projects is scheduling my work and capacity planning for the future.</em></p>

<p>Without the constraints on time, some projects have dragged. Projects running on and on are nothing new, but it does make things extremely difficult when trying to schedule future work or bring new projects on board.</p>

<p>Perhaps the most most obvious way to resolve this is to time-box work. However, my worry is that by putting an artificial time-limit back on the scope, it will be all too easy to fall back into some form of time-based billing.</p>

<p>I’ve recently had success selling sprints purely based on scope — customers love the safety that the price won’t change, as it is based on the value of the sprint. So whilst weekly billing seems to be the simplest answer, I’m obviously reluctant to go down this route.</p>

<p>However, I’ve begun to realise there’s perhaps a bigger takeaway.</p>

<blockquote>
<p>Value pricing is forcing me to completely rethink what my business does.</p>
</blockquote>

<p>Those generic development services (“programmer for hire”) are now getting in the way of growing my business.</p>

<p>This realisation probably started when I introduced Service Plans, which have been great for me and my customers. They offer a known scope of work for a known monthly price.</p>

<p>The majority of my Service Plans customers have no time restrictions, but are instead restricted on the scope of work that’s included. Lower-tier plans might include monitoring and maintenance, security patches, etc., whilst higher plans include new feature development, consulting, R&amp;D, and so on.</p>

<p>Service Plans are designed for the modern apps, whose environment changes every day (OS upgrades, security patches, user feedback, bugs, changing APIs, etc.)</p>

<p>However, there’s still much more I can do in this area.</p>

<h1 id="moving-to-productised-services">Moving to Productised Services</h1>

<p>With this in mind, I’ve begun reducing the number of “traditional” or ad-hoc development projects I’ve taken on, and have concentrated on the Service Plans.</p>

<p>As I’ve learned more about my customers’ needs, I’ve begun to identify common requirements which can be packaged into fixed-price, fixed-scope “productised services”.</p>

<p>Productised services and value pricing seem to go hand-in-hand. In the coming days, I’ll be publishing a new site detailing the first set of services.</p>

<p><img src="https://assets.chrisblunt.com/2016/11/16/psw-screenshot.png" alt="Early Productised Services Landing Page — Coming Soon!">
<em>Early Productised Services Landing Page — Coming Soon!</em></p>

<p>With known scope and pre-defined outcomes, I hope to be able to schedule work more confidently, and offer a far more solid value proposition to new and existing customers. The monthly Service Plans will continue to be the main focus, but over time I hope to refine what’s included in the plans (beyond the code) to offer even more value to customers.</p>

<hr>

<p>Have you started using value pricing in your business? Do you get hit with the fear when asked to give a price? I’d love to hear how you’re conquering those fears and using value pricing in your business. Let me know by <a href="https://www.plymouthsoftware.com/contact">getting in touch</a>.</p>
]]>
      </description>
      <pubDate>Wed, 16 Nov 2016 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/scheduling-and-capacity-planning-with-value-pricing</link>
      <guid>https://www.chrisblunt.com/scheduling-and-capacity-planning-with-value-pricing</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Rails on Docker: Quickly Create or Update Your Gemfile.lock</title>
      <description>
        <![CDATA[<p>As I migrate all of my Rails apps to Docker, I've learned to avoid making changes to the Gemfile whenever possible.</p>

<p>Whilst there are workarounds that can help to speed things up, any change to your <code>Gemfile</code> results in a lengthy (re)download and install of all the gems in your app.</p>

<p>Sometimes I only needed to generate a new <code>Gemfile.lock</code> so that the image could be built, tested and published by our continuous deployment server. Frustratingly, this meant having to wait for the redundant local <code>bundle install</code> to complete before I could commit the new lock file.</p>

<hr>

<p>Today, I discovered a way to save the hours wasted downloading gems: bundler's <strong>lock</strong> command.</p>

<p>This gem[^1] of a command resolves your app's dependencies and writes out the appropriate <code>Gemfile.lock</code> – without installing any of the gems themselves.</p>

<h2 id="using-bundle-lock-with-docker">Using Bundle Lock with Docker</h2>

<p>Use the following command in your app's path to quickly create your <code>Gemfile.lock</code>:</p>
<pre data-language="bash"># cd /path/to/my-rails-app

$ docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app ruby:2.3.1 bundle lock</pre>
<p>If you have an existing lock file, but want to update the gems (equivalent to <code>bundle update</code>), just add the <code>--update</code> flag:</p>
<pre data-language="bash"># cd /path/to/my-rails-app

$ docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app ruby:2.3.1 bundle lock --update</pre>
<p>Hopefully this will save you some time (and bandwidth) when building your Rails apps on Docker.</p>

<p>[^1]: Pun very much intended</p>
]]>
      </description>
      <pubDate>Wed, 20 Jul 2016 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-on-docker-quickly-create-or-update-your-gemfile-lock</link>
      <guid>https://www.chrisblunt.com/rails-on-docker-quickly-create-or-update-your-gemfile-lock</guid>
      <category>bundler</category>
      <category>docker</category>
      <category>rails</category>
    </item>
    <item>
      <title>The Value Conversation</title>
      <description>
        <![CDATA[<p>On my <a href="https://www.chrisblunt.com/hello-brave-new-world">journey to value pricing</a>, I’ve regularly seen and heard the term value conversation mentioned when describing early interactions with the customer.</p>

<p>To begin with, I thought this was just another name for the initial call or meet with a potential new customer. The usual meet-and-greet over coffee, getting a broad overview of their idea, listening to their requirements, and trying to formulate in my head (as quickly as possible) some solutions.</p>

<p>After a while, though, I began to see that these initial consultations were, at their worst, actually limiting my work and the value I delivered by solving only surface-level concerns — which often aren’t even a problem!</p>

<p>As I’ve had more and more of these initial conversations over the weeks and months, I’ve come to realise how different they are from a <em>true value conversation</em>.</p>

<h1 id="digging-into-the-problem">Digging into the Problem</h1>

<p>Let’s start by defining the <em>value conversation</em> as I’ve come to understand it: a true value conversation is far more exploratory, and goes into far more depth than a typical initial consultation — even though it may still be called as such.</p>

<p>By way of an example, I’ll summarise the typical traditional initial discussion I was using:</p>

<p>A potential customer would get in touch via phone or email with a brief outline of task they want to achieve. This may be a cold enquiry, or a referral. The potential customer may have idea for a new app; need some help with a specific technical issue; or even need some new work carrying out on an existing project.</p>

<p>After answering some initial qualifying questions via email, a phone call or meeting is booked to find out more about the project. What generally happened, though, is that “finding out more” is almost entirely focussed on the technical details of the initial enquiry.</p>

<p>The customer says they need a mobile app, so we start discussing what platforms would be best suited, what sort of infrastructure we might need , third-party API integrations, and so on. In some cases, we’d even start napkin-sketching some design and layout ideas (giving away value!)</p>

<p>As I gained more experience, I began to introduce questions to find out a little more about the problem (e.g. what made you think about this idea?, or what is the problem with the current app?), but invariably, any attempts to discover what’s driven the project were either limited to surface level problems — glossed over as a necessary evil, or even entirely brushed aside to get to the excitement of the solution.</p>

<p>At the end of all this, the potential customer asks for some “ballpark” idea of costs and timescales, and numbers are <s>carefully considered</s> plucked from the air and proffered. Even though they requested a ballpark, <strong>always know that the customer hears a final price</strong>.</p>

<p>After pleasantries are exchanged, some written communication and formalisation of the discussion takes place, and the project is either won or lost. Then we get to dive into the fun stuff (writing code, designing screens, pushing releases). Within a few weeks — once code has begun to generate results, and things are “real” — that initial discussion and the initial “problem” we set out to solve become a long-forgotten memory.</p>

<p>The real problem — lurking in the shadows all the time, but buried under layers of <em>really great features</em> — may never get solved. I’ve seen enough projects that start with such great intention, but go on to solve little or even nothing at all.</p>

<p>Clearly, this approach is damaging for everyone involved.</p>

<h1 id="focus-on-the-problem">Focus on the Problem…</h1>

<p>So let’s take a step back, and try to explain how a value conversation can help. The difference is in how the relationship starts. A real value conversation establishes a level of partnership between the customer and the consultant. It is in the consultant’s interest to dive as deep into the root cause(s) of the project — the real pain that it is trying to solve. Similarly, it is in the customer’s best interest to realise this themselves, and be able to talk with the consultant about it.</p>

<p>In this way, both parties are working towards a common goal rather than being at odds, and both parties can see the true value in what is being built. In the future, when questions arise about why something is being done, it can always be measured against that initial root problem, and approved or rejected. A bonus of this — for both consultant and customer — is that it can help to contain scope creep / scope seep.</p>

<p>So, in practice, a value conversation is far more exploratory than a traditional initial consult. The goal (as a consultant) is simply to listen, and keep questioning the reasons behind a customer’s problem. Solutions should be avoided at this stage, or kept minimal.</p>

<h1 id="but-allow-ideas-to-flow">…but allow ideas to flow</h1>

<p>However, in my experience, once some root problems have been uncovered, it’s inevitable that my mind starts thinking of solutions. These will usually be small ideas that may solve the customer’s problem.</p>

<p>For example, if the customer is looking to integrate some email marketing solution into their app, then one relatively simple idea is to use an off-the-shelf product or service, rather than writing their own, and having to deal with all the problems and ongoing maintenance that entails.</p>

<p>There are two possible outcomes here. One is that it’s something the customer hadn’t considered, and after some more exploration, it might be that that is enough to solve their need. Except for some short-term integration work, this would be a fairly trivial solution that gets the job done. The customer has received value from the conversation, and that might be as far as the project goes (for now).</p>

<p>Alternatively, they might put up a barrier to the solution — perhaps their IT department refuses to allow third-party systems to be integrated, etc. — and thus reveal a deeper problem to the consultant.</p>

<p>All of this is valuable feedback for the consultant, helping them to build a picture of the true value of the project and what they can deliver.</p>

<h1 id="actions">Actions</h1>

<p>For a while now, with all of this in mind, I’ve been trying to move my initial discussions towards a value conversation model. Basically trying to shut my mind up (it tends to start hunting for solutions immediately) and take a lot more time getting an understanding of a customer’s real requirements.</p>

<p>An interesting outcome of this is that I’ve found myself actively trying to put off new customers — finding ways to talk them out of hiring us. If it wasn’t for <a href="http://artofvalue.com/do-not-assume-in-a-value-conversation/">Jonathan Stark talking about exactly this</a>, then I would be very concerned that something is going wrong…after all, actively discouraging work is generally not good business!</p>

<p>But in my experience, doing exactly that has actually helped to refine the work I’ve taken on. It seems to help customers justify their requirements to themselves, and in doing so better ensures they are getting genuine value from working with me, rather than work for work’s sake.</p>

<p>My work has evolved to spending more time helping customers to understand their true need at these early stages of a project’s inception. The code is just an outcome of that understanding.</p>

<p>By obtaining a much deeper understanding of the problems behind their project, everyone is on the same page. We’re no longer jumping to solutions and chasing false hopes dreams. We’re consistently working towards a common goal and delivering real value, all from simply changing how we frame those initial discussions.</p>

<hr>

<p>Have you changed your initial discussions to value conversations? How have you found the experience? Do you actively try to discourage new leads from working with you? Let me know by <a href="https://www.chrisblunt.com/contact">getting in touch</a>.</p>
]]>
      </description>
      <pubDate>Fri, 10 Jun 2016 12:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/the-value-conversation</link>
      <guid>https://www.chrisblunt.com/the-value-conversation</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Value First, Price Last</title>
      <description>
        <![CDATA[<p>Once you start pricing work based on value delivered rather than time incurred, more is affected than just the figures in your proposal. It shapes how you think, act and operate your entire business.</p>

<p>This has been my experience as I transition <a href="https://www.plymouthsoftware.com/">my business</a> to a 100% value pricing model. When you can no longer fall back on time-and-materials, you are forced to look at your business in a different way and to figure out the real value you deliver.</p>

<p>This is not to say that it is easy to do. Whilst simple value pricing (if such a thing exists) might be to charge a percentage of monetary return on investment, that is not always something that is available. In fact, it may not even be the primary motivation for a project.</p>

<p>You need to be able to determine what is valued by each individual customer for each project, and that’s something that can take time and require thinking about things a little differently.</p>

<h1 id="start-with-value">Start with Value</h1>

<p>When you can no longer fall back on your time, you have to change your approach to business to establish value early on in any engagement. In any other business, this would be the norm, but for some reason, in knowledge businesses we seem to get stuck worrying about our own costs and time rather than what we’re delivering to the customer.</p>

<p>Doing things this way, it’s easy to work backwards: What’s the budget?…What’s our rate?…Here’s what we guess we’ll do in that amount of time. The price becomes the primary and ongoing concern.</p>

<p>Time billing means it is very easy to spend a lot of time (and money) delivering nothing — and expecting to get rewarded for it. In any other context — indeed, in any other type of business — this would be crazy. <strong>Imagine paying £30k for a new car, but finding only half of it turns up because “our engineers ran out of time”</strong>.</p>

<p>Value pricing doesn’t let you get away with this form of <em>lazy</em> pricing. It forces you to really understand your own business before you even start working with a customer.</p>

<p>In making the switch, I’ve had to completely rethink how Plymouth Software is positioned; the services we offer; and the outcomes of those services to our customers — what we deliver.</p>

<p>In a very short time, I’ve been forced to completely change my business model in three key areas:</p>

<h1 id="position">Position</h1>

<p>If you define your business by the deliverables (a web site designer, for example), your business has a very broad, generic target customer. In fact, if you’re anything like I was, your target customer is <em>anyone who wants an app/website/whatever</em>.</p>

<p>This is a problem as it means you have to redefine your value proposition to every new customer for every new job, and you can easily end up attracting the wrong customers to your business.</p>

<p>When you start thinking about your ideal client — who is it you actually want to work with; what are their hopes, dreams and worries (what my business mentor calls their 2am issues) — you can begin to figure out what they really value.</p>

<p>It took me a little while to realise what I was really doing was positioning my business. This is a whole subject in itself, and something that Philip Morgan covers in his work — I recommend checking out his blog and books if you’re interested.</p>

<p>It took me a little while to realise what I was really doing was positioning my business. This is a whole subject in itself, and something that Philip Morgan covers in his work — I recommend checking out his blog and books if you’re interested.</p>

<h1 id="product-isation">Product(isation)</h1>

<p>Once you have your ideal customer in mind, and have gained an understanding of what they value, you can begin to standardise your offer.</p>

<p>One extreme of this would be an off-the-shelf product of some sort. For software companies, that’s usually a SaaS or downloadable resource such as a paid app, theme, eBook, and so on. However, there’s often a huge amount of upfront investment and risk required to make the leap from client-work to the <em>dream product</em>.</p>

<p>At this stage, I’m productising my consulting efforts — something I first learned about through <a href="http://casjam.com/">Brian Casel</a>. Productised consulting sits somewhere in the middle-ground of off-the-shelf and bespoke work. By providing a common structure around your service offering, you can begin to match your services to your customers’ goals. I’ve started this by productising the early stages of any engagement.</p>

<p>As an example, after an initial review, we now follow up with short phone consultation to discuss a business idea, we follow up with a code-review and/or a full-day discovery workshop (depending on the project).</p>

<p>The outcomes from these allow us to dig into a project and put together a realistic and value-based delivery proposal for our customers. Both of these services are offered on a fixed price basis, with a number of scope options to match customers’ needs.</p>

<p>This is by no means a new concept, and all of these services require a lot of input. However, by structuring them in this way, we can tweak and adapt them to better suit the needs of our target customers as we learn more about them.</p>

<h1 id="price">Price</h1>

<p>It’s perhaps surprising that price is the last item to be considered, but it is with good reason.</p>

<p>Only once you’ve figured out your ideal customer, established what are their goals and needs, and honed your business services to meet those goals, can you begin to calculate the value you deliver — and derive a price.</p>

<p>That price cannot be derived by a simple formula. It requires conversations with your customer. Setting the price for your services <a href="https://artofvalue.com/">becomes an art</a> — something that can change with every project; and something that actually feels good to present.</p>

<p>If you have taken the time to understand the value of your work, you know that your price is fair and mutually beneficial to everyone involved.</p>

<h1 id="the-benefits-of-value-first">The Benefits of Value First</h1>

<p>Thinking about the value you deliver forces you to reflect on your business in a way that’s easy to brush under the carpet when charging for time. The benefits are immeasurable, though. You become more confident in your business offering; you can standardise (and thus document) your operations and workflows; you can identify an ideal customer and align your business with their goals.</p>

<p>Most importantly, your customers receive real value from working with you, and you build stronger relationships as a result. This can only come from figuring out the value first, and making the price a result of that value rather than the driving force behind it.</p>

<p>It’s a difficult, long and ongoing process, but the switch to value pricing does more than just change the figures in your proposal — it transforms everything about your business.</p>

<hr>

<p>Have you made the switch to value pricing? How has it affected your own business and the way you operate? Are you worried about making the switch away from time billing? Let me know by <a href="https://www.chrisblunt.com/contact">getting in touch</a>.</p>
]]>
      </description>
      <pubDate>Wed, 30 Mar 2016 12:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/value-first-price-last</link>
      <guid>https://www.chrisblunt.com/value-first-price-last</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Value Pricing Progress Update March 2016</title>
      <description>
        <![CDATA[<p>At the start of this year, I <a href="https://www.chrisblunt.com/hello-brave-new-world/">committed</a> to making the switch entirely to value pricing. After a couple of months, I’ve practised the approach on a few projects and enquiries, with some notable successes.</p>

<h1 id="mini-project">Mini-Project</h1>

<p>In February, I proposed a small project to an existing customer. They had requested some additional functionality that was outside the scope of their existing service plan.</p>

<p>In this instance, there were two distinct pieces of work available, so that made for 2 easy options. Thinking of a third option to present was difficult, so unfortunately I fell back to a traditional “buy both and get a lower total price” option.</p>

<p>In the end, the customer chose the middle option, which was the larger of the two pieces of work. Although I wasn’t particularly happy with my third option (which simply offered a reduced total price by committing to two projects instead of one), the act of proposing a fixed-price value agreement was extremely refreshing.</p>

<p>Most importantly, other than target delivery dates, the proposal included no mention of time-cost. Instead, the scope of work was defined as end-user outcomes rather than technical deliverables.</p>

<p>Concentrating on goals rather than implementation has made for a much smoother-running project. As the work has progressed, the implementation has adapted but there has been no need to worry about additional time as everything has been in line with those original user goals.</p>

<p>I can’t say that time was entirely absent from my pricing decision at this stage — I did give consideration to my internal costs when pricing the work, but did not expose this in the proposal. Taking a cue from <a href="https://expensiveproblem.com/">Jonathan Stark</a>, I priced the project such that minor tweaks and changes would not be an issue. It is part of the <a href="https://www.chrisblunt.com/the-value-of-a-fixed-fee/">value of offering a fixed-fee agreement</a>.</p>

<h1 id="new-service-plan-agreement">New Service Plan Agreement</h1>

<p>In this first quarter, we also won another value-based service plan agreement. This had been in the works a little while beforehand, but during the proposal period I offered the customer a value-based alternative to the more traditional hours-per-month retainer approach.</p>

<p>Again, a few options were presented, based on the levels of scope and access that would be available. We eventually settled on a middle-ground agreement between options 1 and 2.</p>

<p>For both this service agreement, which is an recurring service, and the discrete project above, I applied what I’ve learned in value pricing to offer a fixed-fee agreement to the customer, and weighted the fees according to the additional benefits and reduction in risk they were receiving.</p>

<h1 id="simplified-proposals">Simplified Proposals</h1>

<p>Whilst I know I’ve made some mistakes in these first value-based proposals, there has undoubtedly been a huge advantage to working in this way.</p>

<p>Putting together the proposals took a fraction of the time that is usually necessary, as they were simply a formal, written summary of what had been agreed in earlier conversations.</p>

<p>Without the need to break down time and deliverables, I was free to focus on how each option I presented could offer increasing value the value to the customer.</p>

<p>Each proposal was therefore only a couple of sides of A4 paper, rather than reams of technical breakdown that would be necessary under a time-based agreement.</p>

<p>It certainly helped my own mindset knowing that the proposals presented solutions to the customers’ business goals, rather than simply increasing in the number of days and weeks that I would be available to the customer.</p>

<h1 id="freedom-to-explore">Freedom to Explore</h1>

<p>By not worrying about the time and minutiae of (technical) deliverables, and instead concerning myself with achieving the overall goals that were defined in the scope, it has freed me to explore alternative options and put in the extra time necessary to research better ways of doing things.</p>

<p>For example, the service plan includes scope for complete redevelopment of an Android application, which is now underway. Without the clock running, I’ve been able to spend some time carrying out in-depth research into how to effectively tackle some of the business goals the app will meet, without feeling the pressure to keep within a set budget of hours.</p>

<p>The biggest challenge has been identifying all the value that could be delivered, and formulating options that truly reflect that. In the coming weeks and months, I’ll be concentrating on developing my skills in holding the <em>value conversation</em>, and training my mind to stop jumping straight to technical solutions.</p>

<hr>

<p>Have you made the switch to value pricing? How has it affected your own business and the way you operate? Are you worried about making the switch away from time billing? Let me know by <a href="https://www.chrisblunt.com/contact">getting in touch</a>.</p>
]]>
      </description>
      <pubDate>Tue, 08 Mar 2016 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/progress-update-march-2016</link>
      <guid>https://www.chrisblunt.com/progress-update-march-2016</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Rails: Generating Polymorphic URLs when Model name does not match Route</title>
      <description>
        <![CDATA[<p>Given a slugged generic model for which you want to generate a URL at runtime, you can usually just call:</p>
<pre data-language="ruby">@b = BlogPost.find(params[:id])
# ==&gt; BlogPost[id: 100, 
polymorphic_url(b)
# ==&gt; blog_post_url(b.id)
# ==&gt; "/blog_posts/hello-world"</pre>
<p>However, I recently came across a case where the named route did not match the model name. Using the above as an example:</p>
<pre data-language="plain"># config/routes.rb
resources :blog_posts, as: 'posts'</pre>
<p>Unfortunately, calling <code>polymorphic_path</code> now throws an error complaining that the generated path method (<code>blog_post_url</code>) does not exist. The path name, which is inferred through the model class <code>BlogPost</code> no longer matches any routes.</p>

<p>Instead, we needed the method to be <code>post_url</code>.</p>

<p>Thankfully, <code>polymorphic_url</code> lets you supply the path as a symbol, rather than inferring it from the model. We can then pass the additional information (in this case, the slug parameter) to the helper as an argument:</p>
<pre data-language="ruby">polymorphic_url([b.model_name.human.underscore.to_sym], id: b.slug)

==&gt; post_url(b.id)
==&gt; "/posts/hello-world"</pre>
]]>
      </description>
      <pubDate>Mon, 29 Feb 2016 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/rails-polymorphic-url</link>
      <guid>https://www.chrisblunt.com/rails-polymorphic-url</guid>
      <category>howto</category>
      <category>programming</category>
      <category>rails</category>
    </item>
    <item>
      <title>The Value of a Fixed Fee</title>
      <description>
        <![CDATA[<p>One of the biggest obstacles I’ve found switching to value pricing is offering a fixed fee for work that has so many unknowns.</p>

<p>As developers, we learn pretty quickly that no two projects are the same, each bringing its own challenges and nuances. (I’m sure this applies to other knowledge/service businesses as well).</p>

<p>It’s perfectly reasonable for a prospective client to ask “how much will this cost?”, but this can be a very difficult question to answer, especially in the early stages of a project where so much is unknown.</p>

<p>Nobody likes surprise invoices, or not knowing how much they will end up spending on a project. Whilst I always preferred to give a fixed fee — and did so in the early days — I soon switched to time-and-materials approach.</p>

<p>At some point, it seemed this was the only way to deal with increasingly complicated projects, unknown requirements, and scope creep (or, far more often the case, <a href="http://www.contrarianconsulting.com/scope-seep/">scope seep</a>).</p>

<h1 id="hidden-time-and-materials">Hidden Time and Materials</h1>

<p>I thought I had found a good compromise to a simple ‘hourly rate’ by using daily, weekly or “per-sprint” rates as a way to package up the time into small, fixed-fee units of work.</p>

<p>However, it soon became clear that this was just re-framing the problem, and would lead to the same questions from customers. “How many hours?” just became “How many days, weeks, or sprints?”</p>

<p>The underlying problem was that the fixed fees I was giving were still based on an <em>estimate of time</em>.</p>

<p>By pricing in this way, I was still stuck on the cost-plus model. The price a customer paid bore no relation to the value being delivered, and brought with it all the same problems as time-and-materials billing normally would.</p>

<p>It certainly did nothing to mitigate expanding scope and managing the unknown — but that’s the subject for another post.</p>

<h1 id="moving-back-to-fixed-fees">Moving Back to Fixed Fees</h1>

<p>There is no question in my mind that offering a fixed fee for work is better for everyone involved, <em>but a fixed fee based on time is not</em>.</p>

<p>So how can you offer a fixed fee, and still protect yourself from the dangers of run-on projects, scope creep and the unknown? It’s taken me perhaps a year to come to terms with the fact that you simply can’t. There will always be a risk in this type of work — it is the nature of our business.</p>

<p><strong>Offering a fixed price means you are taking on that risk, but by doing so, you are adding value for your customer.</strong></p>

<p>So if you are value pricing, you can and should charge a premium for offering a fixed price and removing that risk for the customer.</p>

<h1 id="a-minimum-price">A Minimum Price</h1>

<p>Even knowing this, though, there’s little doubt that determining a value-derived price is difficult — especially so in these early days.</p>

<p>Despite a conscious effort to avoid it, it’s all too easy to fall back into thinking hours and rates.</p>

<p>To help overcome this, I plan to use time-based estimates to give myself a baseline price. This will simply show the minimum level that my value-derived price must match for the work to be viable.</p>

<p><em><strong>Aside</strong>: I am part-way through Ron Baker’s excellent book, <a href="http://www.amazon.co.uk/Implementing-Value-Pricing-Business-Professional/dp/0470584610/ref=sr_1_1">Implementing Value Pricing</a>. Ron introduces the idea of a reservation price, which takes this concept much further.</em></p>

<p>If the value to the customer cannot exceed the time-based estimate, then the work won’t be taken on.</p>

<p>For the work we do take on, the customer gets a value-based, fixed price (based on output), and I can still monitor the costs (based on input) to the business.</p>

<ul>
<li>Pricing based on estimated time is just cost-plus based pricing. A price should instead be based on the agreed value to the customer.</li>
<li>A fixed fee loads the risk onto the consultant, but this in turn represents added value to the customer.</li>
<li>Value pricing should ignore time, but in these early days it’s difficult to escape the mindset of hourly rates. For now, I’ll use my time estimates as the minimum value that must be met to take on piece of work.</li>
</ul>

<hr>

<p>Have you made the switch to value pricing? How has it affected your own business and the way you operate? Are you worried about making the switch away from time billing? Let me know by <a href="https://www.chrisblunt.com/contact">getting in touch</a>.</p>
]]>
      </description>
      <pubDate>Mon, 22 Feb 2016 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/the-value-of-a-fixed-fee</link>
      <guid>https://www.chrisblunt.com/the-value-of-a-fixed-fee</guid>
      <category>value-pricing</category>
    </item>
    <item>
      <title>Android: Toggling Your App's Theme</title>
      <description>
        <![CDATA[
  <strong>24 February 2016:</strong>
    The Android Support library now includes a <code>Theme.AppCompat.DayNight</code>
    theme which bundles this feature for free. <a href="http://android-developers.blogspot.co.uk/2016/02/android-support-library-232.html">See the Android Developer Blog post</a> for details.<p></p>


<p>I was recently asked how an app's theme can be toggled by the user in Android. This is a nice feature that is often seen in reading apps.</p>

<p>I've always found Android's approach to theming overwhelmingly complex, but with the Android 5 Lollipop release and <a href="http://android-developers.blogspot.co.uk/2015/05/android-design-support-library.html">Design Support Library</a>, Google have made great progress in simplifying things.</p>

<p>Recent builds of Android Studio have also included an early Theme Editor which can be used to edit and preview your app's themes and how it will affect various widgets.</p>

<p>In this tutorial, we'll build a simple app with a switch that lets the user toggle between dark and light themes.</p>

<div class="center mw6">
  <img src="/assets/images/2015/11/FE2177CC-EC6A-44D3-B493-7B1373B87A9A-24628-00001BD57A2F760B-1.gif" alt="Theme Toggling Screenshot" class="mw-100">
</div>

<hr>

<p>The full source code for this tutorial is available at <a href="https://github.com/cblunt/blog-android-theme-toggler">https://github.com/cblunt/blog-android-theme-toggler</a>.</p>
<pre data-language="bash">$ git clone git@github.com:cblunt/blog-android-theme-toggler.git</pre>
<hr>

<h2 id="create-the-project">Create the project</h2>

<p><em>Note: For this project, I'm using the latest Android Studio 2.0 beta release (preview2).</em></p>

<p>We'll start by creating a new application:</p>

<ul>
<li><p>Create a new project called <em>ThemeToggler</em> targetting API Level 15 (Ice Cream Sandwich). We'll use the Android Design Support library to support older devices running Android 4.</p></li>
<li><p>Choose <strong>Blank Activity</strong> to create a basic Activity with a Floating Action button.</p></li>
<li><p>Accept the defaults for the remaining screens to create your new application.</p></li>
</ul>

<h2 id="build-the-ui">Build the UI</h2>

<p>Once generated, we'll add a switch widget for the user to toggle the app's theme:</p>

<ul>
<li>Delete the <em>Hello World</em> label from the default layout that was created.</li>
<li>Add a new <code>Switch</code> widget to the layout with the following attributes:</li>
</ul>
<pre data-language="xml">&lt;!-- res/layout/content_main.xml --&gt;
&lt;Switch
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="Toggle Theme"
  android:id="@+id/switch1"
  android:layout_alignParentTop="true"
  android:layout_alignParentLeft="true"
  android:layout_alignParentStart="true"
  android:layout_alignParentRight="true"
  android:layout_alignParentEnd="true" /&gt;</pre>
<p>Next, we'll hook up our switch to toggle the application's theme:</p>

<ul>
<li>Add an <code>OnCheckedChangeListener</code> to the Switch in your Activity. The state of the Switch will be used to determine which theme is used:</li>
</ul>
<pre data-language="java">// app/src/main/java/com/example/themetoggler/MainActivity.java
public class MainActivity extends AppCompatActivity {
  // ...
  protected void onCreate(Bundle savedInstanceState) {
    // ...
    Switch toggle = (Switch) findViewById(R.id.switch1);

    toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
      @Override
      public void onCheckedChanged(CompoundButton view, boolean isChecked) {
        toggleTheme(isChecked);
      }
    });
  }</pre>
<h2 id="saving-the-user-s-choice">Saving the User's Choice</h2>

<p>When the theme is toggled, we'll need to restart the current Activity to use the chosen theme. This is similar to configuration changes in Android (e.g. screen rotation) where the system will destroy and recreate the current Activity.</p>

<p>We'll also need to store the user's chosen theme so the re-created Activity knows which theme to use. It makes sense to store this in the app's <code>SharedPreferences</code> so that the user's choice is persisted across application launches.</p>

<p>After storing the user's preference, we store a reference to the Activity's original Intent, finish the current activity and restart it using that original Intent:</p>

<ul>
<li>Add a new method, <code>toggleTheme()</code> to MainActivity to store the user's preference and restart the current Activity:</li>
</ul>
<pre data-language="java">// app/src/main/java/com/example/themetoggler/MainActivity.java
public class MainActivity extends AppCompatActivity {
  private static final String PREFS_NAME = "prefs";
  private static final String PREF_DARK_THEME = "dark_theme";

  // ...

  private void toggleTheme(boolean darkTheme) {
    SharedPreferences.Editor editor = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit();
    editor.putBoolean(PREF_DARK_THEME, darkTheme);
    editor.apply();

    Intent intent = getIntent();
    finish();

    startActivity(intent);
  }
}</pre>
<h2 id="creating-the-theme">Creating the Theme</h2>

<p>Finally, we'll need to change the Activity's <code>onCreate</code> method to use the user's preferred theme.</p>

<p>However, we haven't created any alternative themes yet, so let's do that first.</p>

<p>Let's add an alternative <code>AppTheme.Dark</code> which extends the default <code>Theme.AppCompat</code>. We can use the original <code>AppTheme</code> that Android Studio created as the basis for our theme:</p>

<ul>
<li>Create <code>AppTheme.Dark</code> in <code>styles.xml</code>:</li>
</ul>
<pre data-language="xml">&lt;!-- app/src/main/res/values/styles.xml --&gt;
&lt;style name="AppTheme.Dark" parent="Theme.AppCompat"&gt;
  &lt;!-- Dark theme base colours --&gt;
  &lt;item name="colorPrimary"&gt;@color/darkColorPrimary&lt;/item&gt;
  &lt;item name="colorPrimaryDark"&gt;@color/darkColorPrimaryDark&lt;/item&gt;
  &lt;item name="colorAccent"&gt;@color/darkColorAccent&lt;/item&gt;
&lt;/style&gt;</pre>
<ul>
<li>We'll also need to override some of the system themes, such as the <code>NoActionBar</code> theme. Use the existing <code>AppTheme.</code> styles as a base:</li>
</ul>
<pre data-language="xml">&lt;!-- app/src/main/res/values/styles.xml --&gt;
&lt;style name="AppTheme.Dark.NoActionBar"&gt;
  &lt;item name="windowActionBar"&gt;false&lt;/item&gt;
  &lt;item name="windowNoTitle"&gt;true&lt;/item&gt;
&lt;/style&gt;

&lt;style name="AppTheme.Dark.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /&gt;
&lt;style name="AppTheme.Dark.PopupOverlay" parent="ThemeOverlay.AppCompat" /&gt;</pre>
<ul>
<li>I've also used declared colours in the theme. Put these in your <code>colors.xml</code> resource file:</li>
</ul>
<pre data-language="xml">&lt;!-- app/src/main/res/values/colors.xml --&gt;
&lt;color name="darkColorPrimary"&gt;#1e2756&lt;/color&gt;
&lt;color name="darkColorPrimaryDark"&gt;#141831&lt;/color&gt;
&lt;color name="darkColorAccent"&gt;#2aac4b&lt;/color&gt;</pre>
<p>We've now declared enough for our themes to be used in the app. You can use Android Studio's experimental Theme Editor to preview and tweak your themes:</p>

<ul>
<li>Choose <strong>Tools-&gt;Android-&gt;Theme Editor</strong>
</li>
<li>In the Theme dropdown, choose your <code>AppTheme</code> and <code>AppTheme.Dark</code> themes to preview how different widgets will look:</li>
</ul>

<p><img src="/assets/images/2015/12/android-studio-theme-editor.png" alt="Theme Editor Preview"></p>

<h2 id="applying-the-theme">Applying the theme</h2>

<p>Finally, back in our code, we can instruct our Activity to use the selected theme in <code>onCreate()</code>.</p>

<p>Note that the code must be at the top of <code>onCreate()</code> (before the call to <code>super.onCreate()</code>) so that the app's default theme (specified in <code>AndroidManifest.xml</code>) is overridden</p>
<pre data-language="java">// app/src/main/java/com/example/themetoggler/MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
  // Use the chosen theme
  SharedPreferences preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
  boolean useDarkTheme = preferences.getBoolean(PREF_DARK_THEME, false);

  if(useDarkTheme) {
    setTheme(R.style.AppTheme_Dark_NoActionBar);
  }

  super.onCreate(savedInstanceState);
  // ...</pre>
<ul>
<li>Also in <code>onCreate()</code>, we'll update the switch to reflect the current choice (make sure this line goes <em>before</em> you add the <code>onCheckedChangedListener</code> to prevent an infinite loop:</li>
</ul>
<pre data-language="java">// app/src/main/java/com/example/themetoggler/MainActivity.java
Switch toggle = (Switch) findViewById(R.id.switch1);
toggle.setChecked(useDarkTheme);
toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
// ...</pre>
<p>Finally, run your app and tap the switch to toggle between your dark and light themes. With every toggle, the Activity will be restarted.</p>

<hr>

<p>This tutorial introduces how you can give the user a choice of theme for your app. The code you added for applying theme (at the top of <code>onCreate()</code>) would be common across all the Activities in your app.</p>

<p>For this reason, it would be a good thing to extract into a <code>BaseActivity</code> from which all your app's Activities inherit.</p>

<p>I hope you've found this tutorial useful. Please let me know your thoughts and feedback in the comments below, or by <del>getting in touch on Twitter</del>.</p>

<p><strong>Full Source Code:</strong> <a href="https://github.com/cblunt/blog-android-theme-toggler">https://github.com/cblunt/blog-android-theme-toggler</a></p>
]]>
      </description>
      <pubDate>Fri, 04 Dec 2015 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/android-toggling-your-apps-theme</link>
      <guid>https://www.chrisblunt.com/android-toggling-your-apps-theme</guid>
      <category>android</category>
      <category>howto</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Using TinyMCE Content CSS with Rails Asset Pipeline</title>
      <description>
        <![CDATA[<p>Recently, whilst building a small CMS system using
<a href="http://tinymce.com/">TinyMCE</a> and the <a href="https://github.com/spohlenz/tinymce-rails">tinymce-rails
gem</a>, I needed to inject some custom
CSS so that the editor's styles matched those of the site.</p>

<p>TinyMCE lets you do this using the <code>content_css</code> option:</p>
<pre data-language="coffeescript"># app/assets/javascript/tinymce.coffee
tinymce.init {
  selector: ".tinymce",
  content_css: ["/assets/tinymce-content.css"],
  # ...
}</pre>
<p>Unfortunately, whilst this is fine in <em>development</em>, where CSS and other assets
are compiled on demand, when deploying to a server and running under
<em>production</em>, the URL above becomes invalid, causing 404 errors and broken
styles.</p>

<h2 id="linking-to-precompiled-content-css">Linking to Precompiled Content CSS</h2>

<p>To ensure the content CSS assets are compiled, we first need to add them to the
precompiled assets list:</p>
<pre data-language="ruby"># config/initializers/assets.rb
Rails.application.config.assets.precompile += %w( tinymce-content.css )</pre>
<p>Next, we need to ensure that the <code>content_css</code> option uses the compiled asset's
URL. We can do this by using a snippet of <code>ERB</code> in our coffeescript file:</p>
<pre data-language="coffeescript"># app/assets/javascript/tinymce.coffee.erb
tinymce.init {
  selector: ".tinymce",
  content_css: ["&lt;%= asset_url 'tinymce-content.css' %&gt;"],
  # ...
}</pre>
<p><em>Note that you also need to add the <code>.erb</code> extension to the filename.</em></p>

<p>With that, the correct compiled URL for <code>tinymce-content.css</code> will be inserted
into your <code>tinymce.init</code> call, and the styles will be loaded correctly into the
editor.</p>

<h3 id="testing-it-out-in-development">Testing it out in development</h3>

<p>You can check the correct styles are loaded in <em>development</em> when precompiled by
running:</p>
<pre data-language="bash">$ bin/rake assets:precompile</pre>
<p>Then restart your server and load the page with your editor.</p>

<p>Don't forget to remove the precompiled assets with:</p>
<pre data-language="bash">$ bin/rake assets:clobber</pre>
]]>
      </description>
      <pubDate>Tue, 08 Sep 2015 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/using-tinymce-content-css-with-rails-asset-pipeline</link>
      <guid>https://www.chrisblunt.com/using-tinymce-content-css-with-rails-asset-pipeline</guid>
      <category>howto</category>
      <category>programming</category>
      <category>rails</category>
      <category>tinymce</category>
    </item>
    <item>
      <title>Rails: Rendering Dynamic SVG Images in respond_to</title>
      <description>
        <![CDATA[<p>SVG is a vector image described in XML. This post shows how to to render SVG images inline with Rails.</p>

<p>This can be used, for example, in an html <code>img</code> tag to generate charts, icons and other dynamic graphics.</p>

<p><strong>1. Register a new MIME type for the SVG response</strong></p>
<pre data-language="ruby"># config/initializers/mime_types.rb
Mime::Type.register "image/svg+xml", :svg</pre>
<p><strong>2. Register a route to view your generated SVG</strong></p>
<pre data-language="ruby"># config/routes.rb
get "icons/generate" =&gt; "icons#generate", format: :svg</pre>
<p><strong>3. Generate your SVG</strong></p>

<p>Here I'm generating the SVG inline, but it could come from anywhere (e.g. <code>model.as_svg</code>).</p>

<p><em>Be sure to include the xml namespace (xmlns) in the <code>svg</code> tag, otherwise browsers just render the content as XML.</em> This caught me out for a little while.</p>
<pre data-language="ruby">class IconsController &lt; ApplicationController
  def generate
    svg = '&lt;svg width="300px" height="300px" xmlns="http://www.w3.org/2000/svg"&gt;
&lt;circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /&gt;
&lt;/svg&gt;'
  end
end</pre>
<p><strong>4. Add your Respond Block</strong></p>

<p>In your controller, add a respond to block, render the SVG.</p>
<pre data-language="ruby">def generate
# ...svg = ...

  respond_to do |format|
    format.svg { render inline: svg}
  end
end</pre>
<p>To view the generated SVG, start the server and visit <a href="http://localhost:3000/icons/generate.svg">http://localhost:3000/icons/generate.svg</a></p>
]]>
      </description>
      <pubDate>Thu, 04 Sep 2014 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/rails-rendering-dynamic-svg-images-in-respond_to</link>
      <guid>https://www.chrisblunt.com/rails-rendering-dynamic-svg-images-in-respond_to</guid>
      <category>howto</category>
      <category>programming</category>
      <category>rails</category>
      <category>svg</category>
    </item>
    <item>
      <title>Android: Consuming a Remote JSON API with Volley</title>
      <description>
        <![CDATA[
    <strong>12 September 2017:</strong> This continues to be one of my most popular posts, but the code was outdated. This updated version includes
    instructions for modern versions of Android, Android Studio, and dependencies
    such as Volley.


<p>In the <a href="/android-getting-started-with-volley">previous tutorial</a>, you learned how to use
Volley's built-in networking tools to quickly download a network resource and
display the results.</p>

<p>Volley is a networking library that removes lots of boiler-plate code when
dealing with handling data across a network. It provides a number of helpful
classes to simplify downloading and parsing data and images, and lets you
concentrate on the details of your app rather than dealing with low-level
networking technologies.</p>

<p>In this post, we'll start a new project and go further into Volley, using the built-in <code>JSONObjectRequest</code> and <code>ImageRequest</code> classes to make use of the remote data. We'll also use Volley's custom <code>NetworkImageView</code> widget to easily (and safely) load a remote image into our UI.</p>

<div class="center mw6">
  <img src="https://assets.chrisblunt.com/2014/08/volley-app-2-finished.jpg?v=2" alt="Volley App Screenshot" class="mw-100">
</div>

<h2 id="create-a-new-android-studio-project">Create a new Android Studio Project</h2>

<p>As in the previous tutorial, we'll use <a href="https://developer.android.com/sdk/installing/studio.html">Android Studio</a> to build the
project. Once installed, create a new project for our app using
the Android New Project setup:</p>

<p><strong>→ Create a new project in Android Studio with the following settings:</strong></p>

<ul>
<li>Open Android Studio and choose <strong>Start a New Android Studio Project</strong>
</li>
<li>Name your new app <code>VolleyApp2</code> under the <code>example.com</code> company name (this will automatically set your package name to <code>com.example.volleyapps2</code>).</li>
<li>Choose <strong>Phone and Tablet</strong> and <strong>API 21: Android 5.0 (Lollipop)</strong> for the Minimum SDK. This will automatically configure your app to use the Android Support Libraries to support older devices.</li>
<li>Click <strong>Next</strong> and then <strong>Basic Activity</strong>. Later we'll add a <code>ListView</code> to browse the remote data.</li>
<li>Name your activity <code>ImagesActivity</code>, and layout <code>activity_images</code>.</li>
<li>Check <strong>Use a Fragment</strong>.</li>
<li>Finally, click <strong>Finish</strong> to create your new project.</li>
</ul>

<p>Once your project has been created and Gradle has updated, hit <strong>Build-&gt;Run</strong>
(Ctrl+R) to test the new app. You should see the standard <strong>Hello World!</strong>
message on your device or emulator.</p>

<h2 id="add-volley-dependency-to-your-gradle-file">Add Volley Dependency to your Gradle File</h2>

<p>Volley is now officially available via Gradle from the <a href="https://github.com/google/volley">Android Open Source Project</a> (this tutorial previously used <a href="https://github.com/mcxiaoke/android-volley">this mirror</a>, which is now deprecated).</p>

<p><strong>→ Add the Volley dependency to your module's <code>build.gradle</code> file:</strong></p>
<pre data-language="groovy">// /app/build.gradle

// ...

dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'com.android.support:appcompat-v7:25.3.1'
  // ...
  compile 'com.android.volley:volley:1.0.0'
}</pre>
<p>Android Studio will notify you that it needs to Sync the Gradle config. This ensures that your project's configuration matches that of Gradle within Android Studio. Synchronising will also download the Volley as a dependency, making its classes available within our app.</p>

<p>If you don't see this notification, choose <strong>Build-&gt;Rebuild Project...</strong> to refresh
and sync the project.</p>

<h2 id="create-a-data-model">Create a Data Model</h2>

<p>We'll set up a simple UI to hold our list of images and their titles. This will use the standard <code>Adapter</code> pattern that is common throughout Android.</p>

<p>Let's start by creating a data model to represent the downloaded data. When
consuming a web service, you'll likely want to store a local cache of the
downloaded data for using offline. For Android apps, this is likely to be a
SQLite database to persist the database.</p>

<p>For this tutorial, though, we'll just cache the data - a list of image URLs and
their titles - into an <code>ArrayList</code> of simple <code>ImageRecord</code> objects.</p>

<p><strong>→ Create a new class, <code>ImageRecord</code> with fields for the image URL and title:</strong></p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImageRecord.java

public class ImageRecord {
  private String url;
  private String title;

  public ImageRecord(String url, String title) {
    this.url = url;
    this.title = title;
  }

  public String getTitle() {
    return title;
  }

  public String getUrl() {
    return url;
  }
}</pre>
<p><strong>→ Create a new class, <code>ImageRecordsAdapter</code>:</strong></p>

<p>This will hold our list of <code>ImageRecords</code> and adapt them to
<code>ImagesActivityFragment</code> UI.</p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImageRecordsAdapter.java
public class ImageRecordsAdapter extends ArrayAdapter&lt;ImageRecord&gt; {
  public ImageRecordsAdapter(Context context) {
      super(context, R.layout.image_list_item);
  }
}</pre>
<p>We haven't yet created the <code>image_list_item</code> layout. Android Studio will notice this and warn us to create the resource. Do that now using the following simple layout.</p>

<p><strong>→ Create a new layout file for <code>image_list_item</code>:</strong></p>
<pre data-language="xml">&lt;!-- /app/src/main/res/layout/image_list_item.xml --&gt;
&lt;?xml version="1.0" encoding="utf-8"?&gt;

&lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"&gt;

  &lt;com.android.volley.toolbox.NetworkImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:scaleType="centerCrop"
    tools:src="@mipmap/ic_launcher"
    android:id="@+id/image1"/&gt;

  &lt;TextView
    android:padding="8dp"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:textAppearance="?android:textAppearanceLarge"
    tools:text="Image Name"
    android:id="@+id/text1"/&gt;
&lt;/LinearLayout&gt;</pre>
<p>Notice the use of <code>com.android.volley.toolbox.NetworkImageView</code> instead of the normal <code>ImageView</code> widget. <code>NetworkImageView</code> is a special widget provided by Volley that adds a simple <code>setImageUrl</code> method.</p>

<p>Volley will automatically handle downloading remote images into the <code>NetworkImageView</code> for us, cancelling any requests in progress if the <code>NetworkImageView</code> is scrolled off the screen.</p>

<p><strong>→ Add a new <code>ImageRecordsAdapter</code> field (<code>mAdapter</code>) inside <code>ImagesActivityFragment</code>:</strong></p>

<p>You'll also initialise it in the fragment's <code>onActivityCreated</code> method.</p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImagesActivity.java

// ...
  public static class ImagesActivityFragment extends Fragment {
    private ImageRecordsAdapter mAdapter;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
      super.onActivityCreated(savedInstanceState);

      mAdapter = new ImageRecordsAdapter(getActivity());

      ListView listView = (ListView) getView().findViewById(R.id.list1);
      listView.setAdapter(mAdapter);
    }
  }</pre>
<p><strong>→ Open the fragment's layout and remove the placeholder <code>TextView</code>, replacing it with a <code>ListView</code> to display the list of images as they are loaded:</strong></p>
<pre data-language="xml">&lt;!-- /app/src/main/res/layout/fragment_image.xml --&gt;
&lt;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".ImagesActivity$PlaceholderFragment"&gt;

  &lt;ListView
    android:id="@+id/list1"
    android:layout_width="match_parent"
    android:layout_height="match_parent" /&gt;
&lt;/RelativeLayout&gt;</pre>
<p>Running the app, you should now see a blank screen. It's time to fetch some remote data using <code>Volley</code> and populate our list!</p>

<h2 id="fetch-a-remote-json-feed-using-volley">Fetch a remote JSON feed using Volley</h2>

<p>We'll use Volley in the same way as <a href="/android-getting-started-with-volley">before</a> to grab a JSON feed. This feed contains a list of images and their titles in the following format:</p>
<pre data-language="javascript">{
  'images': [
    {
      'url': "http://assets.example.com/image1.jpg",
      'title': "Sunset"
    },
    {
      'url': "http://assets.example.com/image2.jpg",
      'title': "Beach"
    },
    // ...
  ]
}</pre>
<p>Start by extending the Android <code>Application</code> class and adding a global <code>RequestQueue</code> field (see the <a href="/android-getting-started-with-volley">previous tutorial</a> for more details on this).</p>

<p><strong>→ Create a new <code>VolleyApplication</code> class:</strong></p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/VolleyApplication.java

public class VolleyApplication extends Application {
    private static VolleyApplication sInstance;

    private RequestQueue mRequestQueue;

    @Override
    public void onCreate() {
        super.onCreate();

        mRequestQueue = Volley.newRequestQueue(this);

        sInstance = this;
    }

    public synchronized static VolleyApplication getInstance() {
        return sInstance;
    }

    public RequestQueue getRequestQueue() {
        return mRequestQueue;
    }
}</pre>
<p><strong>→ Set the new <code>VolleyApplication</code> class in your app's <code>AndroidManifest.xml</code> file:</strong></p>

<p>Now is also a good time to add the <code>android.permission.INTERNET</code> permission,
so our app can download the remote JSON file.</p>
<pre data-language="xml">&lt;!-- /app/main/AndroidManifest.xml --&gt;
&lt;!-- ... --&gt;
  &lt;uses-permission android:name="android.permission.INTERNET" /&gt;

  &lt;application
    android:name=".VolleyApplication"
    ...&gt;
  &lt;/application&gt;</pre>
<p>With an accessible, global <code>RequestQueue</code> ready to receive Volley requests, we can now add a new private method <code>fetch</code> to <code>ImagesActivityFragment</code>.</p>

<p><strong>→ Add the following <code>fetch()</code> method:</strong></p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImagesActivity.java

// ...

  public static class ImagesActivityFragment extends Fragment {
    // ...
    private void fetch() {
      JsonObjectRequest request = new JsonObjectRequest(
        "http://cblunt.github.io/blog-android-volley/response2.json",
        null,
        new Response.Listener&lt;JSONObject&gt;() {
          @Override
          public void onResponse(JSONObject jsonObject) {
            // TODO: Parse the JSON
          }
        },
        new Response.ErrorListener() {
          @Override
          public void onErrorResponse(VolleyError volleyError) {
            Toast.makeText(getActivity(), "Unable to fetch data: " + volleyError.getMessage(), Toast.LENGTH_SHORT).show();
          }
      });

      VolleyApplication.getInstance().getRequestQueue().add(request);
    }
  }</pre>
<p><strong>→ Add a call to <code>fetch()</code> at the end of <code>onActivityCreated</code>:</strong></p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImagesActivity.java

// ...

  public static class ImagesActivityFragment extends Fragment {
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
      mAdapter = new ImageRecordsAdapter(getActivity());

      ListView listView = (ListView) getView().findViewById(R.id.list1);
      listView.setAdapter(mAdapter);

      fetch();
    }</pre>
<h2 id="parsing-the-data">Parsing the Data</h2>

<p>The above code sends a remote request (asychronously) via Volley and calls back with a response, either a successfully parsed <code>JSONObject</code> from the response, or an error (such as a 404).</p>

<p>In our <code>onResponse</code> handler, we'll forward the parsed <code>JSONObject</code> onto a new method, <code>parse</code> which will convert the JSON feed into a list of <code>ImageRecord</code>s.</p>

<p><strong>→ Add the <code>parse()</code> method to <code>ImagesActivityFragment</code>:</strong></p>

<p>This method to loop through the array of images in the JSON feed, and create a
new <code>ImageRecord</code> for each one.</p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImagesActivity.java

// ...

  public static class ImagesActivityFragment extends Fragment {
    // ...
    private List&lt;ImageRecord&gt; parse(JSONObject json) throws JSONException {
      ArrayList&lt;ImageRecord&gt; records = new ArrayList&lt;&gt;();

      JSONArray jsonImages = json.getJSONArray("images");

      for(int i =0; i &lt; jsonImages.length(); i++) {
        JSONObject jsonImage = jsonImages.getJSONObject(i);
        String title = jsonImage.getString("title");
        String url = jsonImage.getString("url");

        ImageRecord record = new ImageRecord(url, title);
        records.add(record);
      }

      return records;
    }
  }
</pre>
<p><strong>→ Update the <code>onResponse()</code> handler to call our new <code>parse()</code> method
and update the <code>ArrayAdapter</code>:</strong></p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImagesActivity.java

// ...

  public static class ImagesActivityFragment extends Fragment {
    // ...
    private void fetch() {
      JsonObjectRequest request = new JsonObjectRequest(
        "http://cblunt.github.io/blog-android-volley-2/response.json",
        null,
        new Response.Listener&lt;JSONObject&gt;() {
          @Override
          public void onResponse(JSONObject jsonObject) {
            try {
              List&lt;ImageRecord&gt; imageRecords = parse(jsonObject);

              mAdapter.swapImageRecords(imageRecords);
            }
            catch(JSONException e) {
              Toast.makeText(getActivity(), "Unable to parse data: " + e.getMessage(), Toast.LENGTH_SHORT).show();
            }
          }

          // ...
    }</pre>
<p>Next, we need to add the <code>swapImageRecords()</code> method to <code>ImageRecordsAdapter</code>.
This simply clears the existing data, adds all the new records, and notifies the
adapter that the underlying data has been updated.</p>

<p><strong>→ Add the <code>ImageRecords()</code> method:</strong></p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImageRecordsAdapter.java

  public void swapImageRecords(List&lt;ImageRecord&gt; objects) {
    clear();

    for(ImageRecord object : objects) {
        add(object);
    }

    notifyDataSetChanged();
  }</pre>
<p>Before the app can be run, we need to override <code>ImageRecordAdapter</code>'s
<code>getView()</code> method to specify how the data should be adapted to the UI.</p>

<p><strong>→ Override <code>getView</code> and inflate the <code>image_list_item</code> layout:</strong></p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImageRecordsAdapter.java

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
      if(convertView == null) {
          convertView = LayoutInflater.from(getContext()).inflate(R.layout.image_list_item, parent, false);
      }

      // NOTE: You would normally use the ViewHolder pattern here
      NetworkImageView imageView = (NetworkImageView) convertView.findViewById(R.id.image1);
      TextView textView = (TextView) convertView.findViewById(R.id.text1);

      ImageRecord imageRecord = getItem(position);

      imageView.setImageUrl(imageRecord.getUrl(), mImageLoader);
      textView.setText(imageRecord.getTitle());

      return convertView;
  }</pre>
<p>Notice the use of <code>setImageUrl</code> to load an image directly into the <code>NetworkImageView</code>. This is provided by Volley.</p>

<p>Just as with the <code>JSONObjectRequest</code>, we need to let Volley know which
<code>RequestQueue</code> should be used to manage the downloaded images.</p>

<p>For images, though, Volley provides another layer of abstraction - the
<code>ImageLoader</code> class. This takes a <code>RequestQueue</code> and an object implementing the
<code>ImageLoader.ImageCache</code> interface.</p>

<p>The <code>ImageCache</code> lets us store downloaded images in memory, only triggering a
network download if image isn't present in the cache.</p>

<h2 id="caching">Caching</h2>

<p>Unfortunately, Volley doesn't provide a built-in cache, so we'll need
to write our own. Fortunately, this is relatively simple using the <code>LruCache</code>
(least—recently used) class provided by Android.</p>

<p><strong>→ Create a new class, <code>BitmapLruCache</code>, with the following code:</strong></p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/BitmapLruCache.java

public class BitmapLruCache extends LruCache&lt;String, Bitmap&gt;
    implements ImageLoader.ImageCache {

  static int getMaxCacheSize() {
      int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
      return maxMemory / 8;
  }

  public BitmapLruCache() {
    super(getMaxCacheSize());
  }

  @Override
  protected int sizeOf(String key, Bitmap value) {
    return value.getRowBytes() * value.getHeight();
  }

  @Override
  public Bitmap getBitmap(String url) {
    return get(url);
  }

  @Override
  public void putBitmap(String url, Bitmap bitmap) {
    put(url, bitmap);
  }
}
</pre>

  <h2 class="mt-1" id="disk-level-caching">Disk-Level Caching</h2>

  Volley provides built-in disk-level caching as it caches network requests. However, this is dependent on the server's response cache headers being set correctly (e.g. the `Expires` header), as this is how Volley determines if an item should be re-downloaded).

  If the server response headers are incorrect or not set, we need to provide our own disk-based cache. A very thorough implementation that could be used is [Jake Wharton's DiskLruCache](https://github.com/JakeWharton/DiskLruCache)


<p>With a simple in-memory cache ready to use, we can now create an new instance
of <code>ImageLoader</code> for our adapter to use.</p>

<p><strong>→ Add the following code to create an instance of <code>ImageLoader</code> in the
adapter's constructor:</strong></p>
<pre data-language="java">// /app/src/main/java/com/example/volleyapp2/ImageRecordsAdapter.java

public class ImageRecordsAdapter extends ArrayAdapter&lt;ImageRecord&gt; {
  private final ImageLoader mImageLoader;

  public ImageRecordsAdapter(Context context) {
    super(context, R.layout.image_list_item);

    mImageLoader = new ImageLoader(VolleyApplication.getInstance().getRequestQueue(), new BitmapLruCache());
  }

  // ...
}</pre>
<p>With that, your app is now ready to download and parse downloaded data, and any external images in the feed. The <code>NetworkImageView</code> (and <code>ImageLoader</code>) will automatically retrieve and display the images.</p>

<p><strong>→ Run your app, and you will be presented with the results of
the parsed feed:</strong></p>

<div class="center mw6">
  <img src="https://assets.chrisblunt.com/2014/08/volley-app-2-finished.jpg?v=2" alt="Volley App Screenshot" class="mw-100">
</div>

<h2 id="consuming-remote-json-apis">Consuming Remote JSON APIs</h2>

<p>You have successfully used Volley to consume a simple JSON API. Although there's
lots more that can be done (e.g. disk caching, locally caching and synchronising
the retrieved data using SQLite, etc.), this tutorial has shown the core of what
needs to be done.</p>

<h2 id="complementing-volley-with-picasso">Complementing Volley with Picasso</h2>

<p>Although Volley provides some great helpers for downloading, caching and displaying images from the network, it can be a little low-level sometimes. I've recently been using Square's <a href="http://square.github.io/picasso/">Picasso library</a> to handle the image downloading side of things.</p>

<p>Not only does Picasso provide a very nice API, it handles disk and memory based caching without any extra configuration or effort from you - and provides some nice built-in transition effects.</p>

<p>Picasso is a topic for another post, but you can find out more at <a href="http://square.github.io/picasso/"></a><a href="http://square.github.io/picasso/">http://square.github.io/picasso/</a>.</p>

<h2 id="next-steps">Next Steps</h2>

<p>I hope you've found this tutorial helpful for learning about Android's powerful
new networking framework. Using Volley to handle your network requests removes
vast swathes of boiler-plate code and edge-case tests which means you can
concentrate on developing your app's valuable features!</p>

<p><a href="https://github.com/cblunt/blog-android-volley-2">Clone the code on github</a></p>
]]>
      </description>
      <pubDate>Wed, 13 Aug 2014 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/android-consuming-a-remote-json-api-with-volley</link>
      <guid>https://www.chrisblunt.com/android-consuming-a-remote-json-api-with-volley</guid>
      <category>android</category>
      <category>api</category>
      <category>howto</category>
      <category>json</category>
      <category>networking</category>
      <category>programming</category>
      <category>restful</category>
      <category>tutorial</category>
      <category>volley</category>
    </item>
    <item>
      <title>Android: Getting Started with Volley</title>
      <description>
        <![CDATA[<p>Volley is a library developed by Google for fast, easy networking in Android. It wraps up a lot of boiler plate code that you would normally have to code yourself when connecting to servers, queuing requests, handling errors, caching, etc.</p>

<p>In this post, you'll learn how to use Volley to perform a basic download of remote data. This will form the basis of consuming a simple RESTful API.</p>

<p><a href="https://github.com/cblunt/blog-android-volley">Clone the code on github</a></p>

<p><strong>A Note on Android Studio</strong></p>

<p>Whilst it's still very much in Beta, I'm a fan of Android Studio and the new Gradle build system for Android. I use it for all day-to-day Android work without too many issues. In this tutorial, I'll be using Android Studio &amp; Gradle.</p>

<p>If you haven't tried it yet, you can <a href="http://developer.android.com/sdk/installing/studio.html">download Android Studio from android.com</a>. At the time of writing,  Android Studio is at version 0.5.7.</p>

<h3 id="creating-the-project">Creating the project</h3>

<p>With Android Studio running, create a new Blank Activity project. After naming your project, using the default settings will be fine:</p>

<p><img src="https://assets.chrisblunt.com/2014/05/Screen-Shot-2014-04-30-at-22.59.03-1024x7151.png" alt="New Project Settings"></p>

<h3 id="add-the-volley-library">Add the Volley Library</h3>

<p>When your app project is created, we can add Volley to the list of dependencies via Gradle. Somewhat strangely, Google don't provide a build of Volley for Gradle, but thankfully there are mirrored copies of Volley published for use in Gradle.</p>

<p>We can add one of these mirrors from <a href="https://github.com/mcxiaoke/android-volley">https://github.com/mcxiaoke/android-volley</a>:</p>
<pre data-language="groovy">// /app/build.gradle
// ...

dependencies {
  // ...
  compile 'com.mcxiaoke.volley:library:1.0.+'
}</pre>
<p>Select <strong>Tools &gt; Android &gt; Sync Project with Gradle Files</strong> (or hit the toolbar icon) to update your project and expose the Volley classes to your code.</p>

<h3 id="create-a-requestqueue">Create a RequestQueue</h3>

<p>With Volley in our project, we can start using it to download remote content. Volley uses the concept of <code>RequestQueue</code> to manage requests for content and download them. We can create a global request queue for our application in a customer <code>Application</code> subclass.</p>

<p>Create a new <code>VolleyApplication</code> class with a <code>RequestQueue</code> field. We'll initialise the <code>RequestQueue</code> in the application's <code>onCreate</code> method, and keep a static instance of <code>VolleyApplication</code> to let us access the queue from anywhere in our app:</p>
<pre data-language="java">// src/main/java/com/example/volleyapp/app/VolleyApplication.java
// ...

class VolleyApplication extends Application {

  private static VolleyApplication sInstance;

  private RequestQueue mRequestQueue;

  @Override
  public void onCreate() {
    super.onCreate();

    mRequestQueue = Volley.newRequestQueue(this);

    sInstance = this;
  }

  public synchronized static VolleyApplication getInstance() {
    return sInstance;
  }

  public RequestQueue getRequestQueue() {
    return mRequestQueue;
  }
}</pre>
<p>Be sure to update your <code>AndroidManifest.xml</code> to specify <code>VolleyApplication</code> as the application's class. We'll also need to request the <code>INTERNET</code> permission:</p>
<pre data-language="xml">&lt;?xml version="1.0" encoding="utf-8"?&gt;

&lt;manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.volleyapp.app"&gt;
  &lt;uses-permission android:name="android.permission.INTERNET" /&gt;

  &lt;application
    android:name=".VolleyApplication"
    ...&gt;
    ...
  &lt;/application&gt;
&lt;/manifest&gt;</pre>
<h3 id="download-a-json-feed">Download a JSON Feed</h3>

<p>Now we can create a request and pass it to Volley. As well as plain text responses, Volley provides some helper classes to download and parse JSON, which is great when consuming APIs:</p>
<pre data-language="java">// src/main/java/com/example/volleyapp/app/MainActivity.java
public class MainActivity extends ActionBarActivity {
  private TextView mTextView;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      mTextView = (TextView) findViewById(R.id.text1);

      JsonObjectRequest request = new JsonObjectRequest("http://cblunt.github.io/blog-android-volley/response.json", null,
                                                        new Response.Listener&lt;JSONObject&gt;() {

                                                            @Override
                                                            public void onResponse(JSONObject response) {

                                                                mTextView.setText(response.toString());
                                                            }
                                                        },

                                                        new Response.ErrorListener() {

                                                            @Override
                                                            public void onErrorResponse(VolleyError error) {
                                                                mTextView.setText(error.toString());
                                                            }
                                                        }
      );
      VolleyApplication.getInstance().getRequestQueue().add(request);
  }</pre>
<p>In this example, we're fetching the remote and simply displaying it in a <code>TextView</code> (remember to set the TextView id in your <code>activity_main.xml</code> layout file). If something goes wrong, the <code>ErrorListener</code> will display the error message in the <code>TextView</code>.</p>

<p>The second parameter of the <code>JsonObjectRequest</code> is an optional JSONObject to be sent with the request. If set, the request will be submitted as an HTTP POST request, otherwise it will be a GET request. Again, this is helpful if we're dealing with a RESTful API.</p>

<p>Finally, with the request created, we can add it to the app's <code>RequestQueue</code> for Volley to handle. Run your app to see the returned JSON response:</p>

<p><img src="https://assets.chrisblunt.com/2014/05/device-2014-05-01-113222-e1398942526376-300x2291.png" alt="Successful JSON Request"></p>

<h3 id="fast-easy-networking-for-android">Fast, Easy Networking for Android</h3>

<p>With Volley, performing networking takes no time at all. In upcoming posts we'll look at downloading images using Volley's built-in <code>ImageRequest</code>, and finally put everything together to consume a simple JSON API.</p>

<p><a href="https://github.com/cblunt/blog-android-volley">Clone the code on github</a></p>
]]>
      </description>
      <pubDate>Thu, 01 May 2014 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/android-getting-started-with-volley</link>
      <guid>https://www.chrisblunt.com/android-getting-started-with-volley</guid>
      <category>android</category>
      <category>api</category>
      <category>howto</category>
      <category>json</category>
      <category>networking</category>
      <category>programming</category>
      <category>restful</category>
      <category>tutorial</category>
      <category>volley</category>
    </item>
    <item>
      <title>Android: Using Gradle</title>
      <description>
        <![CDATA[<p>On a couple of Android projects I'm working on, I've switched to using the new Gradle Build system. The idea of storing the entire build configuration in one place is great.</p>

<p>My early experience of Gradle seemed to cause more problems than it solved. However, this was probably due to lack of understanding as much as the early status of the tools. As well as taking the time to learn more about Gradle, recent releases of the toolkit also seem far more stable and integrated with the Android Studio/IDEA IDE.</p>

<p>This post is a summary of how I've solved various tasks with Gradle and what I've learned so far. Hopefully it will help guide you if you're getting started with Gradle,  and serve as a handy reminder. These instructions are based on using Android Studio 0.4:</p>

<h4 id="including-an-external-jar">Including an external Jar</h4>

<ol>
<li> Create a <code>libs</code> folder under your project's top-level folder.</li>
<li> Copy the <code>.jar</code> file into the new <code>libs</code> folder, or a sub-folder.</li>
<li> In your app's <code>build.gradle</code>, add the <code>jar</code> as a file dependency:</li>
</ol>
<pre data-language="groovy">// /MyProject/my-app/build.gradle

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:0.8.+'
    }
}

apply plugin: 'android'
repositories {

    mavenCentral()

    flatDir {
        dirs 'libs'
    }
}

android {
  // ...
}

dependencies {
  compile files('libs/somelibrary.jar')
}
</pre>
<h4 id="including-an-external-library-module">Including an external library module</h4>

<ul>
<li> Add a new module at the top level of your project (e.g. <code>MyProject/my-library</code>)</li>
<li> Inside your project's <code>settings.gradle</code>, create a reference to the module. The reference is a path to your module relative to the Project root. Note that you use <code>:</code> as a path seperator rather than <code>/</code>:</li>
</ul>
<pre data-language="groovy">// MyProject/settings.gradle

include ':my-library'
</pre>
<ul>
<li>In your app's <code>build.gradle</code>, add the external library as a project dependency:</li>
</ul>
<pre data-language="groovy">// /MyProject/MyApp/build.gradle

android {
// ...
}

dependencies {
compile project(':my-library')
}</pre>
<h4 id="including-google-play-services">Including Google Play Services</h4>

<p>Google Play Services is increasingly becoming the only way to interact with Google's APIs (this is a good thing, as it will hopefully help to reduce platform fragmentation). The Google Play Services are now provided through a local repository.</p>

<ul>
<li> Ensure that the Google Play Services repository is added in the Android SDK Manager:2.  Add the dependencies to your <code>build.gradle</code> file corresponding to the services you want to use:</li>
</ul>
<pre data-language="groovy">// /MyProject/MyApp/build.gradle

android {
  // ...
}
dependencies {
  compile 'com.google.android.gms:play-services:3.2.+'
}</pre>
<p>The same is also true of the Android Support Libraries which are provided through a separate local repository:</p>
<pre data-language="groovy">// /MyProject/MyApp/build.gradle
dependencies {
  compile 'com.android.support:support-v4:19.0.0'
  compile 'com.android.support:gridlayout-v7:19.0.0'
  compile 'com.android.support:appcompat-v7:19.0.0'
  // ...
}</pre>
<h4 id="product-flavours-and-build-types">Product Flavours and Build Types</h4>

<p>In all the documentation, I couldn't find a explanation of the difference between a product flavour and a build type.</p>

<p>Put simply, a product flavour is a different edition your app that you might release on Google Play, the most common example being a free and paid edition of your app. I've not really used these much, but one thing you can do is configure a different package name for your app depending on which flavour you use,  e.g:</p>

<p>When the app is compiled, it will use the appropriate package name according to the product flavour your have built.</p>

<p>A build type is an environment setting for your app. By default, a debug (development) and release (production) build type are set up. I usually add a third staging build type.</p>

<p>Build types let you set environment. Specific values using the build config object. This is great for setting things like <code>ContentProvider</code> authorities and  API endpoints:</p>
<pre data-language="groovy">// /MyProject/MyApp/build.gradle
android {

  buildTypes {
    debug {
      buildConfigField "String", "PROVIDER_AUTHORITY", "\"com.example.app.debug.provider\""
      buildConfigField "String", "API_ENDPOINT", "\"http://development.localhost/\""
    }

    staging {
      buildConfigField "String", "PROVIDER_AUTHORITY", "\"com.example.app.beta.provider\""
      buildConfigField "String", "API_ENDPOINT", "\"http://beta.example.com/api/\""
    }

    release {
      buildConfigField "String", "PROVIDER_AUTHORITY", "\"com.example.app.provider\""
      buildConfigField "String", "API_ENDPOINT", "\"http://api.example.com/api/\""
    }
  }
}
</pre>
<p>With product flavours and build types, you could generate a <code>debugFree</code> apk of your app for testing, and a <code>releasePaid</code> apk for distribution on the Play Store.</p>

<h4 id="keeping-release-signing-configuration-out-of-source-control">Keeping release signing configuration out of source control</h4>

<p>The keystore and passwords for signing your release jars is now also configured through the Gradle build script. Like any sensitive data, though, you should ensure this is never published to your code repository.</p>

<p>Instead, using <a href="https://gist.github.com/gabrielemariotti/6856974">the code from this gist</a> lets you keep your signing data in an external file which Gradle loads at compile time. I add a <code>signing.properties.example</code> file to my repository to server as a reminder to set the correct values:</p>
<pre data-language="groovy">// /MyProject/MyApp/build.gradle

android {
  // ...
}
dependencies {
  // ...
}

def Properties props = new Properties()
def propFile = new File('signing.properties')

if (propFile.canRead()) {
    props.load(new FileInputStream(propFile))

    if (props != null &amp;&amp; props.containsKey('STORE_FILE') &amp;&amp; props.containsKey('STORE_PASSWORD') &amp;&amp;
            props.containsKey('KEY_ALIAS') &amp;&amp; props.containsKey('KEY_PASSWORD')) {
        android.signingConfigs.release.storeFile = file(props['STORE_FILE'])
        android.signingConfigs.release.storePassword = props['STORE_PASSWORD']
        android.signingConfigs.release.keyAlias = props['KEY_ALIAS']
        android.signingConfigs.release.keyPassword = props['KEY_PASSWORD']
    } else {
        println 'signing.properties found but some entries are missing'
        android.buildTypes.release.signingConfig = null
    }
} else {
    println 'signing.properties not found'
    android.buildTypes.release.signingConfig = null
}</pre><pre data-language="bash"># signing.properties.example

STORE_FILE=/path/to/your.keystore
STORE_PASSWORD=yourkeystorepass
KEY_ALIAS=projectkeyalias
KEY_PASSWORD=keyaliaspassword</pre>
]]>
      </description>
      <pubDate>Wed, 22 Jan 2014 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/android-using-gradle</link>
      <guid>https://www.chrisblunt.com/android-using-gradle</guid>
      <category>android</category>
      <category>gradle</category>
      <category>howto</category>
      <category>programming</category>
      <category>tips</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Android: Getting Started with Databases and ContentProviders Part 1</title>
      <description>
        <![CDATA[<p><a href="https://github.com/cblunt/blog-android-databases-tutorial">Clone the code from GitHub</a></p>

<p>Android provides powerful database functionality built on SQLite. If you're familiar with SQL from web development, you'll be very comfortable with SQLite. However, what you might find is that it's very easy to deal directly with the database (especially if you're coming from a framework like Rails or Symfony).</p>

<p>Whatever language or platform you're using, it's always good practice to build an abstraction layer between your app's code and its database. In this tutorial miniseries, we'll walk through building a simple app that uses a database abstraction class, <code>MyDatabaseHelper</code> to manage querying, inserting, updating and deleting records from a simple database. In future posts, we'll look at dealing with multiple tables and joins, and finally explore Android's own <code>ContentProvider</code> API to abstract our database in a standard way.</p>

<h3 id="getting-started">Getting started</h3>

<p>Start with a new Android application project (I recommend trying out the new <a href="http://developer.android.com/sdk/installing/studio.html">Android Studio preview</a>, especially if you're just starting out with Android), and add the following files:</p>
<pre data-language="java">// src/com/example/MyActivity.java

public class MyActivity extends Activity {
  private MyDatabaseHelper mDatabaseHelper;

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mDatabaseHelper = new MyDatabaseHelper(this);
  }
}</pre><pre data-language="java">// src/com/example/MyDatabaseHelper.java

public class MyDatabaseHelper extends SQLiteOpenHelper {
  public static final String TABLE_USERS = "users";
  public static final String COL_ID = BaseColumns._ID;
  public static final String COL_NAME = "name";
  public static final String COL_EMAIL = "email";
  public static final String COL_DOB = "date_of_birth";
  private static final String DATABASE_NAME = "my_app.db";
  private static final int DATABASE_VERSION = 1;

  public MyDatabaseHelper(Context context) {
    super(context, DATABASE_NAME, null, DATABASE_VERSION);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
    db.execSQL("CREATE TABLE " + TABLE_USERS + " ("
        + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
        + COL_NAME + " TEXT NOT NULL,"
        + COL_EMAIL + " TEXT,"
        + COL_DOB + " INTEGER"
        + ");");
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL("DROP TABLE IF EXISTS " + TABLE_USERS + ";");
    onCreate(db);
  }

  public long insert(String tableName, ContentValues values) {
    return getWritableDatabase().insert(tableName, null, values);
  }

  public int update(String tableName, long id, ContentValues values) {
    String selection = COL_ID + " = ?";
    String[] selectionArgs = {String.valueOf(id)};

    return getWritableDatabase().update(tableName, values, selection, selectionArgs);
  }

  public int delete(String tableName, long id) {
    String selection = COL_ID + " = ?";
    String[] selectionArgs = {String.valueOf(id)};

    return getWritableDatabase().delete(tableName, selection, selectionArgs);
  }
}</pre>
<p>The advantage of this approach is that <code>MyDatabaseHelper</code> becomes a single point of access to the raw database (following good <a href="http://en.wikipedia.org/wiki/Don">DRY</a> principles). It also provides the opportunity to validate data before it is written to the database. For example, we have added a <code>NOT NULL</code> constraint on the <code>name</code> column. If we tried to save a record to the database without setting a value for name, our app would crash with a <code>SQLiteConstraintException</code>. Let's demonstrate this by adding a few users in our Activity, but leaving one of the names blank:</p>
<pre data-language="java">// src/com/example/MyActivity.java

  public void onCreate(Bundle savedInstanceState)
    // ...
    addUser(null, null, 0);
    addUser("Joe User", "joe@example.com", 0);
    addUser("Mary Jones", "mary@example.com", 0);
    addUser("Sue Bloggs", "sue@example.com", 0);
  }

 private void addUser(String name, String email, long dateOfBirthMillis) {
    ContentValues values = new ContentValues();
    values.put(MyDatabaseHelper.COL_NAME, name);

    if (email != null) {
      values.put(MyDatabaseHelper.COL_EMAIL, email);
    }

    if (dateOfBirthMillis != 0) {
      values.put(MyDatabaseHelper.COL_DOB, dateOfBirthMillis);
    }

    mDatabaseHelper.insert(MyDatabaseHelper.TABLE_USERS, values);
  }
</pre>
<p>Running this code, your app will crash with the <code>SQLiteConstraintException</code> we expected:</p>
<pre data-language="bash">  ERROR/SQLiteDatabase(19185): Error inserting name=null

      android.database.sqlite.SQLiteConstraintException: users.name may not be NULL (code 19)
      at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
      at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:775)
      at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
      ....
</pre>
<h3 id="adding-validations">Adding Validations</h3>

<p>Using our abstract Database helper class though, we can protect against this and handle the error much more elegantly. To do this, we'll add some simple validations at the appropriate points in <code>MyDatabaseHelper</code>:</p>
<pre data-language="java">// src/com/example/MyDatabaseHelper.java
  public long insert(String tableName, ContentValues values) throws NotValidException {
    validate(values);
    return getWritableDatabase().insert(tableName, null, values);
  }

  public int update(String tableName, long id, ContentValues values) throws NotValidException {
    validate(values);
    String selection = COL_ID + " = ?";
    String[] selectionArgs = {String.valueOf(id)};

    return getWritableDatabase().update(tableName, values, selection, selectionArgs);
  }

  // ...

  protected void validate(ContentValues values) throws NotValidException {
    if (!values.containsKey(COL_NAME) || values.getAsString(COL_NAME) == null || values.getAsString(COL_NAME).isEmpty()) {
      throw new NotValidException("User name must be set");
    }
  }

  public static class NotValidException extends Throwable {
    public NotValidException(String msg) {
      super(msg);
    }
  }</pre>
<p>Back in <code>MyActivity</code>, wrap the call to <code>insert()</code> in a <code>try...catch</code> block to capture the <code>NotValidException</code> and show a message to the user:</p>
<pre data-language="java">// src/com/example/MyActivity.java
  private void addUser(String name, String email, long dateOfBirthMillis) {
    // ...
    try {
      mDatabaseHelper.insert(MyDatabaseHelper.TABLE_USERS, values);
    } catch (MyDatabaseHelper.NotValidException e) {
      Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
    }
  }</pre>
<p>Now if you run your app, it will gracefully handle the error, and continue adding the other user records to the local database.</p>

<h3 id="querying-and-displaying-records">Querying and Displaying Records</h3>

<p>At the moment, we can't see our data though. We'll add a quick <code>ListView</code> to <code>MyActivity</code> to show the list of user's. If one doesn't already exist, create a new layout resource file in your project: <code>res/layouts/my_activity.xml</code> and add a <code>ListView</code> to fill the screen:</p>
<pre data-language="xml">// res/layout/my_activity.xml

&lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent"&gt;
    &lt;ListView
        android:layout_width="fill_parent"
        android:layout_height="match_parent"
        android:id="@+id/listView"/&gt;
&lt;/LinearLayout&gt;</pre>
<p>In <code>MyActivity</code>, make sure that you set the content view to be the new <code>my_activity.xml</code> layout:</p>
<pre data-language="java">// src/com/example/MyActivity.java

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.my_activity);
    // ...
  }</pre>
<p>Next, create a <code>SimpleCursorAdapter</code> to retrieve data from the database and display it in our new <code>ListView</code>:</p>
<pre data-language="java">// src/com/example/MyActivity.java

  public void onCreate(Bundle savedInstanceState) {
    // ...
    Cursor c = mDatabaseHelper.query(MyDatabaseHelper.TABLE_USERS, MyDatabaseHelper.COL_NAME);

    String[] from = new String[]{MyDatabaseHelper.COL_NAME, MyDatabaseHelper.COL_EMAIL};
    int[] to = { android.R.id.text1, android.R.id.text2 };

    SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, c, from, to, 0);

    ListView listView = (ListView) findViewById(R.id.listView);
    listView.setAdapter(adapter);
    // ...
  }</pre>
<p>Finally, you'll see that we've called a new method, <code>query()</code>, on our database helper. The parameters specify the table to query, and how to sort results. This will provide a simple way to retrieve the records from our database, wrapping the <code>SQLiteOpenHelper</code>'s own <code>query()</code> method. Add the new method to <code>MyDatabaseHelper</code>:</p>
<pre data-language="java">// src/com/example/MyDatabaseHelper.java

  public Cursor query(String tableName, String orderedBy) {
    String[] projection = {COL_ID, COL_NAME, COL_EMAIL, COL_DOB};

    return getReadableDatabase().query(tableName, projection, null, null, null, null, orderedBy);
  }</pre>
<p>Of course, we could add other parameters such as search terms to our <code>query()</code>, making it very powerful - and again providing a single point of contact for querying our database.</p>

<p>Running the code, you'll now see the list of names and email addresses as people are added to the database. Note that every time you re-run the app, another set of records will be added as the calls to <code>addUser()</code> are in the activity's <code>onCreate()</code> method.</p>

<p><img src="https://assets.plymouthsoftware.com/2013/05/databases_tutorial_11-180x300.png" alt="Screenshot of App Running"></p>

<p>As you can see, it's relatively easy to build a powerful abstraction layer between your database and application code, and there are huge advantages in doing so. As your app grows, you'll start to build a library of common methods that will save you time and make your code more robust.</p>

<h3 id="coming-next">Coming Next</h3>

<p>In the next tutorial, we'll add another table to our database and adapt our database helper class to handle multiple tables and joins. In the final part, we'll look at Android's <code>ContentProvider</code> API, which provide a standard way to abstracting and manage access to your your app's raw database.</p>
]]>
      </description>
      <pubDate>Wed, 22 May 2013 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/android-getting-started-with-databases-and-contentproviders-part-1</link>
      <guid>https://www.chrisblunt.com/android-getting-started-with-databases-and-contentproviders-part-1</guid>
      <category>android</category>
      <category>contentprovider</category>
      <category>database</category>
      <category>howto</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Rails 3: Storing Model Metadata Attributes with ActiveRecord::Store</title>
      <description>
        <![CDATA[<p>I recently discovered the excellent <a href="http://api.rubyonrails.org/classes/ActiveRecord/Store.html">ActiveRecord <code>store</code></a> method whilst researching the best-practice for storing optional, flexible metadata against a record. <code>store</code> lets you keep simple key/value data into a single text column on your model.</p>

<p>By declaring stored attributes on your model, ActiveRecord will automatically generate the appropriate setter/accessor methods, and validations work just as you'd expect.</p>
<pre data-language="ruby"># db/migrate/create_cars.rb

# ...

create_table do |t|

  t.references :model

  t.references :manufacturer

  t.text :metadata # Note metadata is just a text column

  t.timestamps

end

# app/models/car.rb

class Car &lt; ActiveRecord::Base

  belongs_to :model

  belongs_to :manufacturer

  # Manufacturer and Model are 'real', database-backed attributes

  attr_accessor :model_id, :manufacturer_id, :colour, :size, :notes, :product_url

  store :metadata, :accessors =&gt; [:colour, :size, :notes, :product_url]

  # Database-backed attributes

  validates :model, :presence, :presence =&gt; true

  validates :manufacturer, :presence =&gt; true

  # Metadata stored attributes

  validates :colour, :presence =&gt; true

  validates :size, :presence =&gt; true, :inclusion =&gt; { :in =&gt; %w(small medium large) }

  validates :product_url, :format =&gt; { :with =&gt; /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/ }

end
</pre>
<p><em>Note that the <code>metadata</code> is just a text column. Rails will automatically serialise the hash into this column.</em></p>

<p>With our <code>Car</code> model defined, and its metadata attributes declared with <code>store</code>, we can use them in just the same way as normal, database-backed attributes:</p>
<pre data-language="ruby">car = Car.new

car.model = CarModel.first

car.manufacturer = Manufacturer.first

car.colour = "Red"

car.size = "medium"

car.product_url = "http://www.example.com/"

car.save

# =&gt; true

car.colour

# =&gt; "red"

car.size

# =&gt; "medium"
</pre>
<h3 id="pros-and-cons">Pros and Cons</h3>

<p>The biggest advantage to this type is store is it provides a flexible data schema. This is perfect for storing non-indexed metadata about a model, and a use-case that often crops up when building apps. It provides a compromise between common relational databases (such as MySQL and PostgreSQL) and the flexibility of NoSQL databases such as MongoDB and CouchDB. In practical use, it means attributes can quickly be added to a model without the need to perform any migrations on the database schema.</p>

<p>A possible disadvantage is that <code>store</code>d attributes can not be indexed or used in queries (as they have no corresponding database column). For example, you could not call <code>Car.where(:colour =&gt; "red")</code>. However, as <a href="http://axonflux.com/one-of-my-favorite-additions-to-rails-3-activ">Garry Tan points out in his post</a>, if this becomes a requirement in the future, you could always add a database-column to the schema at a later date, and just move the data at that time.</p>

<h4 id="references">References</h4>

<ol>
<li> <a href="http://axonflux.com/one-of-my-favorite-additions-to-rails-3-activ">http://axonflux.com/one-of-my-favorite-additions-to-rails-3-activ</a>
</li>
</ol>
]]>
      </description>
      <pubDate>Sun, 06 Jan 2013 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/rails-3-storing-model-metadata-attributes-with-activerecordstore</link>
      <guid>https://www.chrisblunt.com/rails-3-storing-model-metadata-attributes-with-activerecordstore</guid>
      <category>activerecord</category>
      <category>programming</category>
      <category>rails-3</category>
      <category>ruby</category>
      <category>tips</category>
    </item>
    <item>
      <title>Welcoming 2013 - Goals for the New Year</title>
      <description>
        <![CDATA[<p>2012 was the year I said farewell to my twenties, celebrated the birth of our beautiful baby daughter, and started to see some real growth in <a href="http://www.plymouthsoftware.com">my business</a>. Whilst blog posts were evidently sparse, it has been an absolutely incredible year, both personally and professionally. So much has happened that the past 12 months seem to have flown by.</p>

<p>In this post, I'll document a brief summary of what's happened, and offer my thoughts and goals for the new year.</p>

<p><img src="https://assets.chrisblunt.com/2012/12/IMG_20120619_202506-e1356946615784.jpg" alt="IMG_20120619_202506"></p>

<h3 id="the-past-year">The Past Year</h3>

<p>In January, I moved <a href="http://plymouthsoftware.com/">Plymouth Software</a> from the incubating LaunchPad to its own offices on <a href="http://www.tamarsciencepark.com.">Tamar Science Park</a> Scott Building. The business has grown its client base and income over the year, and whilst by no means enough to let me take a <a href="http://www.fourhourworkweek.com/blog/2008/06/04/how-to-take-a-mini-retirement-tips-and-tricks/">mini-retirement</a> yet, feels a lot more solid than 12 months ago.</p>

<p>As well as winning several new clients (commercial, academic and charitable), I also expanded the company's service offer to include development of native iOS applications, and I am currently engaged developing an iPhone app due for release in February 2013.</p>

<p>In June, I ran the <a href="https://plymouthhalfmarathon.co.uk">Plymouth Half Marathon</a>, shaving a couple of minutes off my 2011 time. Thanks to the generosity of friends, family, colleagues, and everyone else who donated, I managed to raise over £600 for <a href="https://www.macmillan.org.uk">Macmillan Cancer Support</a>.</p>

<p>We travelled to Spain, Jersey, Scotland, several cities in Northern England, lots of spots around Devon and Cornwall; and of course, drank plenty of tea.</p>

<p>Of course, the highlight of the year occurred in August, when Claire and I celebrated the arrival of our baby daughter. Becoming a Dad really puts a new perspective on things, which has inevitably changed my thinking for what I want to achieve in the next 12 months.</p>

<h2 id="2013-the-goals">2013: The Goals</h2>

<h4 id="produce-more-consume-less">Produce More, Consume Less.</h4>

<p>With our amazing new addition, running and building Plymouth Software, and generally trying to balance life and business, my time seems to have become incredibly limited. This is evident by the sheer lack of posts here. I realise though that this is just an excuse - it's all too easy to be suckered into the endless river of social media streams, news feeds, blog , etc. and procrastinatory opportunities that present themselves when you run your own business. My first goal then is to focus on producing rather than consuming. The Internet is endless in its ability to offer more, so it's better to try to output (quality) content rather than read it.</p>

<p><strong>Goal:</strong> Publish at least one relatively in-depth blog post to this site each month. Write content for products (see goals below).</p>

<h4 id="focus">Focus</h4>

<p>Lack of focus really hit me this year when I realised just how many <em>task and project management apps</em> I'd signed up for in an effort to become more organised. The time spent trying out what are essentially list apps could have been put to far better use. Again, the endless distraction of the Internet was the cause, offering an escape from dealing with code problems and <em>producing</em>.</p>

<p>Back in 2011, I found the <a href="http://www.pomodorotechnique.com/">Pomodoro technique</a> incredibly useful (using the excellent <a href="http://www.pomodroido.com/">Pomodroido app</a>), but for some reason I stopped. The resulting lack of focus has led to frustration.</p>

<p><strong>Goal:</strong> Start using the Pomorodo technique again to focus on work that_ generates value_. I've also signed up to <a href="http://letsfreckle.com/">Freckle</a> for the quickest way to see where my time is being spent.</p>

<p><strong>Sidenote:</strong> For task management, I settled on <a href="https://www.wunderlist.com/">Wunderlist</a>. With the release of version 2, they've added the absolutely vital Reminders and Repitition to tasks, which were essential. Wunderlist is also multi-platform, and now has a nice native <a href="https://play.google.com/store/apps/details?id=com.wunderkinder.wunderlistandroid&amp;feature=nav_result#?t=W251bGwsMSwyLDNd">Android app</a>.</p>

<h4 id="balance">Balance</h4>

<p>As part of developing Plymouth Software's business model, one of my aims is to ensure a good work-life balance. As more client work has come in and deadlines approach, I've felt more of a drag to do <em>just a few minutes</em> in evenings and weekends.</p>

<p><strong>Goal:</strong> Hit the reset button to keep work at the office. Establish business hours with myself and clients, and stick to them.</p>

<p><strong>Sidenote:</strong> According to <a href="http://www.alistapart.com/articles/fourdayweek/">others' experiences</a>, reducing time available to work talks should actually help to <em>improve</em> my focus and efficiency.</p>

<h4 id="products">Products</h4>

<p>When it's your own, I don't believe <em>selling time</em> is a viable way to build a sustainable business - irrespective of your rate. In order to grow Plymouth Software financially, it needs to a fundamental change away from the freelance model (selling my time) to a <em>business</em> model, sustainable through the sale products, or services which can be packaged as products, that begin to remove my time from the equation.</p>

<p>This has been the aim since day one. I've <a href="https://www.chrisblunt.com/thoughts-on-agile-project-estimating-and-pricing/">tried to figure this out before</a>, but could never make the break away from selling time. The freelance model also provided the quickest way to generate some income and build capital, which is essential for a bootstrapped business. Now, though, in order to grow the company (both financially and taking on staff), this approach needs to change.</p>

<p>Reading about very talented people in similar situations have made the switch inspired me to get thinking, and taking Brennan Dunn's excellent <a href="http://doubleyourfreelancingrate.com/build-a-consultancy">Consultancy Masterclass</a> opened my eyes to a new way of productising my services, as well as offering products that are not a traditional part of the business, such as eBooks, code plugins, and workshops.</p>

<p><strong>Goal:</strong> Schedule time and funds to develop <em>revenue-generating assets</em> (more details on the first of those in the next post).</p>

<h4 id="finance">Finance</h4>

<p>To my mind, self-employment - having the ability to quickly adapt to situations and generate new streams of income - has always been <em>far more</em> <em>stable</em> than being an employee, where your income is firmly in the hands of somebody else. The real advantage of working for yourself is the freedom to be in control of my own future, as well as the sheer enjoyment that comes with building up a business.</p>

<p>Plymouth Software has so far granted an income and turned a profit in Year 1. However, in 2013, with lots of new responsibilities, I would like to at least double the turnover that the company has made in its first year (as a limited company).</p>

<p>This should be easily achievable by fulfilling some of the goals above, as well as following in the footsteps of bigger consultancy firms from whom I take inspiration. For example, I hadn't come across weekly billing until taking Brennan's Consultancy Masterclass course, but it seems to be quite common in software development shops, particularly those practicing agile techniques.</p>

<p><strong>Goal:</strong> Double the company's turnover for Year 2. Acquire at least 4 retainer-style contracts for clients. Set value-based standard rates, and move to a weekly billing model for project/consultancy jobs.</p>

<h4 id="brand-marketing">Brand / Marketing</h4>

<p>I'll be the first to admit the business must improve its marketing output. The <a href="http://plymouthsoftware.com/">company website</a> is functional, but the portfolio is already outdated, and doesn't much promote the core service offering of the company. Along with changes the underlying business model above, I'd like to take the opportunity to refine the company brand itself.</p>

<p>One thing I've recognised is that the business actually offers clients much more than simple code-and-deliver development services. As I've taken on more involved projects, I've found myself consulting on a range of subjects, from business development and marketing, through to experience design and technical environment considerations.</p>

<p>As I move the company away from a freelance model, I need to ensure that the brand reflects the full scope of what the company offers.</p>

<p><strong>Goal:</strong> Reduce friction of updating company website by moving to more flexible content management system. Rewrite marketing copy to focus on full range of services offered. Send at least one email newsletter per month. Write one white-paper on mobile apps, to be distributed though the company web site.</p>

<h4 id="fitness">Fitness</h4>

<p>In both 2011 and 2012 I ran the <a href="http://plymouthhalfmarathon.co.uk">Plymouth Half Marathon</a>, and the fitness that came with running was great. However, with no running goal for 2013 - and a <em>lot</em> of sleepless nights since August - my running has taken a back seat. In 2013, I'd like to rectify this by committing to get back into running, as well as taking advantage of January membership discounts for the local gym and swimming pool.</p>

<p><strong>Goal</strong>: Get back into running at least once a week, minimum of 3 miles per run (road or gym). Gym or Swim session twice a week.</p>

<h3 id="final-thoughts-the-overall-goal">Final Thoughts - The Overall Goal</h3>

<p>Hopefully, as we move to 2014, I'll be able to look back over this post and check off all those goals.</p>

<p>I'm sure though, like any plan, they will change a little throughout the course of 2013, but it feels good to have committed to writing where I want to be. Ultimately, the most important goal (and that which has influenced all others) is to provide and spend as much quality time with our daughter as she grows up.</p>

<p>2013 is going to be a great - Happy New Year!</p>
]]>
      </description>
      <pubDate>Mon, 31 Dec 2012 10:00:00 +0000</pubDate>
      <link>https://www.chrisblunt.com/welcoming-2013-goals-for-the-new-year</link>
      <guid>https://www.chrisblunt.com/welcoming-2013-goals-for-the-new-year</guid>
      <category>business</category>
      <category>personal</category>
    </item>
    <item>
      <title>Running Plymouth Half Marathon - Let's Raise £1k for Macmillan Cancer Support</title>
      <description>
        <![CDATA[<p>Last year, I successfully completed my first Plymouth Half Marathon and raised for local charity Jeremiah's Journey.</p><p>In memory of a close friend, Pat Mourton, who sadly passed away from cancer in August 2011, I am running the Plymouth Half Marathon again on the 3 June 2012. My goal is to raise at least £1k for <a href="http://www.macmillan.org.uk">Macmillan Cancer Support</a>, who gave incredible and ongoing support to both Pat and her family throughout her battles with cancer.</p><p>You can be part of helping me to reach (or beat!) my target, and raise as much as possible for Macmillan by donating on my <a href="http://www.justgiving.com/chrisblunt-plymouth2012">Just Giving</a> page at <a href="http://www.justgiving.com/chrisblunt-plymouth2012">http://www.justgiving.com/chrisblunt-plymouth2012</a>.</p><p>If you can't donate today, then please help spread the word by posting the link on your favourite social networks.</p><p>Thank you!</p><blockquote>
<p>In 1996, Pat Mourton was diagnosed with breast cancer. I was lucky enough to meet Pat through her son Richard and husband Alan, during my first days at secondary school. They quickly became very close friends.</p>
<p>Pat was well known for her work in the local community, and everyone who was fortunate enough to meet her couldn’t help but be inspired by her spirit, determination, and (perhaps most of all) her unfaltering sense of humour.</p>
<p>In the nearly 2 decades she lived with the disease, Pat fought and beat the breast cancer, and bravely endured bone cancer and later metastatic liver cancer. She ran countless fundraising events for the charities that would become close to her heart.</p>
<p>Sadly, Pat lost her battle with cancer on 15 August 2011.</p>
<p>Macmillan nurses provide exceptional support and care not only to cancer sufferers, but their families who face this horrible disease. Macmillan nurses truly went above-and-beyond the call of duty for Pat, Alan and Richard.</p>
<p>In memory of Pat, and to help Macmillan carry on this tireless work, I’m running the Plymouth Half Marathon for a second time on 3 June 2012 and need to raise £1000 for Macmillan.</p>
<p>Please donate whatever you can to help me reach - and smash - my target of raising £1000 for Macmillan Cancer Support so that other people can continue to receive the support that was so important to Pat, Alan and Richard.</p>
<p>Please take a moment to send this page to everyone you know to help support those who are suffering with cancer.</p>
<p>Thank you,</p>
<p>Chris</p>
</blockquote>
]]>
      </description>
      <pubDate>Tue, 01 May 2012 11:00:00 +0100</pubDate>
      <link>https://www.chrisblunt.com/running-plymouth-half-marathon-lets-raise-1k-for-macmillan-cancer-support</link>
      <guid>https://www.chrisblunt.com/running-plymouth-half-marathon-lets-raise-1k-for-macmillan-cancer-support</guid>
      <category>charity</category>
      <category>half-marathon</category>
      <category>macmillan</category>
      <category>personal</category>
      <category>plymouth</category>
      <category>running</category>
    </item>
  </channel>
</rss>
