• TestOps Strategy
  • Posts
  • Github To Dockerhub: A CI/CD Pipeline With Serverspec (Part 1)

Github To Dockerhub: A CI/CD Pipeline With Serverspec (Part 1)

Building a Docker CI Pipeline

This is the first of 3 posts in building a CI Pipeline that validates an image for Caddy and publishes the image to DockerHub. Before it can be publish the image, the pipeline should validate that the image builds a proper container. That’s where Serverspec comes in.

What is Serverspec?

Serverspec makes it easy to write infrastructure tests using RSpec. It will verify that servers or in this case, containers, are configured and behaving as expected.

What is Caddy?

Caddy is an extensible platform that serves web sites, apis and services.

Setting up Serverspec

After writing my Dockerfile, I installed the serverspec gem and ran the serverspec-init command. It will ask for the OS and how to connect to the infrastructure under test.

Select OS type:

  1) UN*X
  2) Windows

Select number: 1

Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 2

 + spec/localhost/
 + spec/localhost/sample_spec.rb

After the set up is done, I opened the sample_spec.rb file and wrote the following test that builds container from the image provided and verify the version of caddy is installed.

require 'docker'
require 'serverspec'
require 'spec_helper'

describe "Caddy is installed" do
  before(:all) do
    image = Docker::Image.build_from_dir('.')

    set :backend, :docker
    set :docker_image, image.id
  end

  describe command('caddy --version') do
    its(:stdout) { should match "v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=\n"}
  end

end

This test does two important things:

  1. Builds the Docker image locally

  2. Asserts that the container contains the exact version of Caddy expected

If the upstream package repository introduces a newer version, this test fails by design.
Let’s, run the test locally:

caddyshack % bundle exec rspec  

Example output:

caddyshack % bundle exec rspec        

Caddy is installed
  Command "caddy --version"
    stdout
      is expected to match "v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=\n"

Finished in 2.71 seconds (files took 0.30817 seconds to load)
1 example, 0 failure

The ServerSpec test validates that the container contains the expected version of Caddy. However, verifying that Caddy is present does not guarantee that the image behaves as intended. Let’s update the spec to validate:

  1. Caddyfile exists in /etc/conf/Caddyshack

  2. Caddyfile is configured properly

  3. Caddy is running based on the Caddyfile

First, let’s add a Caddyfile that will be added to the image in order validate that caddy is running and the tests can pass. For simplicity, when the container is running locally, http://localhost should return ok.

:80 {
  respond "ok" 200
}

Here’s the updated spec that covers the three additional items.

require 'docker'
require 'serverspec'
require 'spec_helper'

describe "Caddy is installed" do
  before(:all) do
    image = Docker::Image.build_from_dir('.')

    set :backend, :docker
    set :docker_image, image.id
  end

  describe command('caddy --version') do
    its(:stdout) { should match "v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=\n"}
  end

  describe "Caddyfile is present" do
    describe file('/etc/caddy/Caddyfile') do
      it { should exist }
      it { should be_file }
      its(:content) { should match(/:80/) }
    end
  end

  describe "Caddy config is valid" do
    describe command('caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile') do
      its(:exit_status) { should eq 0 }
      its(:stdout) { should match(/valid configuration|success|OK/i) }
    end
  end
end

describe "Caddy image runtime behavior" do
  before(:all) do
    @host_port = 8080

    image = Docker::Image.build_from_dir('.')

    @container = Docker::Container.create(
      'Image' => image.id,
      'ExposedPorts' => { '80/tcp' => {} },
      'HostConfig' => {
        'PortBindings' => {
          '80/tcp' => [{ 'HostPort' => @host_port.to_s }]
        }
      }
    )

    @container.start

    # Wait until Caddy is ready (prevents flaky tests)
    30.times do
      break if system("curl -fsS http://localhost:#{@host_port} >/dev/null 2>&1")
      sleep 0.2
    end
  end

  after(:all) do
    @container&.delete(force: true)
  end

  it "returns ok on HTTP request" do
    set :backend, :exec

    response = command("curl -fsS http://localhost:#{@host_port}").stdout
    expect(response).to eq("ok")
  end
end

After running the specs:

Caddy is installed
  Command "caddy --version"
    stdout
      is expected to match "v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=\n"
  Caddyfile is present
    File "/etc/caddy/Caddyfile"
      is expected to exist
      is expected to be file
      content
        is expected to match /:80/
  Caddy config is valid
    Command "caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile"
      exit_status
        is expected to eq 0
      stdout
        is expected to match /valid configuration|success|OK/i

Caddy image runtime behavior
  returns ok on HTTP request

Finished in 4.59 seconds (files took 0.3076 seconds to load)
7 examples, 0 failures

The image and container has been validated in less than 5 seconds. As the Docker image continues to be enhanced, this test suite provides a strong baseline that can grow alongside new features and capabilities

With these ServerSpec checks in place, the image can be validated and is ready to be published to DockerHub. In the next post, this workflow will run as a GitHub Actions so every change is automatically built, tested, and only then published to DockerHub.