How to convert an entrypoint to a Pebble layer

This guide will show you how to take an existing Docker image entrypoint and convert it into a Pebble layer, aka the list of one or more services which is defined in rockcraft.yaml and then taken by the rock’s Pebble entrypoint.

Reference entrypoint

For this guide, the reference Docker image entrypoint will be NGINX. The official Debian-based NGINX image’s Dockerfile can be found here.

In summary, this Dockerfile is basically installing NGINX into the image and then defining the OCI entrypoint to be a custom shell script which parses the first argument given to it at container deployment time, and then configures and launches NGINX accordingly.

Design the Pebble services

A Pebble layer is composed of metadata, checks and services. The latter is present in rockcraft.yaml as a top-level field and it represents the services which are loaded by the Pebble entrypoint when deploying a rock.

Given the reference entrypoint, this guide’s goal is to create two services: one for nginx and another for nginx-debug. The following services snippet does just that:

services:
  nginx:
    override: replace
    startup: disabled
    command: nginx [ -g 'daemon off;' ]
    environment:
      TZ: UTC
    on-failure: shutdown
  nginx-debug:
    override: replace
    startup: disabled
    command: nginx-debug [ -g 'daemon off;' ]
    environment:
      TZ: UTC
    on-failure: shutdown
    

This is defining two separate Pebble services which are disabled by default at startup, have the same environment variable, but are executed with different commands (nginx and nginx-debug).

Build the rock

Copy the above snippet and incorporate it into the rockcraft.yaml file which will be used to build your rock, as shown below:

name: custom-nginx-rock
base: "[email protected]"
version: latest
summary: An NGINX rock
description: |
    A rock equivalent of the official NGINX Docker image from Docker Hub.
license: Apache-2.0
platforms:
    amd64:

package-repositories:
  - type: apt
    url: https://nginx.org/packages/mainline/ubuntu
    key-id: 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
    suites: 
      - jammy
    components:
      - nginx
    priority: always

parts:
  nginx-user:
    plugin: nil
    overlay-script: |
      set -x
      useradd -R $CRAFT_OVERLAY -M -U -r nginx

  nginx:
    plugin: nil
    after:
      - nginx-user
    stage-packages:
      - nginx
      - tzdata

# Services to be loaded by the Pebble entrypoint
services:
  nginx:
    override: replace
    startup: disabled
    command: nginx [ -g 'daemon off;' ]
    environment:
      TZ: UTC
    on-failure: shutdown
  nginx-debug:
    override: replace
    startup: disabled
    command: nginx-debug [ -g 'daemon off;' ]
    environment:
      TZ: UTC
    on-failure: shutdown
    

This Rockcraft recipe is almost fully declarative, with the creation of the “nginx” user being the only scripted step.

To reproduce what the reference NGINX Dockerfile is doing, notice the use of package-repositories in this rockcraft.yaml file, allowing you to also make use of NGINX’s 3rd party package repository (even using the same GPG key ID as the one used in the Dockerfile).

NOTE: to add custom configuration files, you can use the dump plugin.

Now, build the final custom NGINX rock with:

rockcraft pack

You should see something like this:

Launching instance...
Retrieved base [email protected] for amd64
Extracted [email protected]
Refreshing repositories | (4.6s)
Package repositories installed
Executed: pull nginx-user
Executed: pull nginx
Executed: pull pebble
Executed: overlay nginx-user
Executed: overlay nginx
Executed: overlay pebble
Executed: build nginx-user
Executed: skip pull nginx-user (already ran)
Executed: skip overlay nginx-user (already ran)
Executed: skip build nginx-user (already ran)
Executed: stage nginx-user (required to build 'nginx')
Executed: build nginx
Executed: build pebble
Executed: skip stage nginx-user (already ran)
Executed: stage nginx
Executed: stage pebble
Executed: prime nginx-user
Executed: prime nginx
Executed: prime pebble
Executed parts lifecycle
Exported to OCI archive 'custom-nginx-rock_latest_amd64.rock'

Then copy the resulting rock (from the OCI archive format) to the Docker daemon via:

sudo rockcraft.skopeo --insecure-policy copy oci-archive:custom-nginx-rock_latest_amd64.rock docker-daemon:custom-nginx-rock:latest

And finally, run the container:

docker run -d --name nginx-pebble-service -p 8000:80 custom-nginx-rock:latest

The Pebble daemon will start without any NGINX service, although you could still later on ask for either service to be started. For instance, you can start the nginx service by typing:

docker exec nginx-pebble-service pebble start nginx

We could have chosen to make one of the two services run on startup by changing its corresponding startup field value to enabled.

Once you start one of the services, your container should be deployed and running the nginx or nginx-debug service, and you should be able to see the NGINX landing page by accessing port 8080 on you localhost:

curl localhost:8000

For which you should see the following output:

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>