Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started with Raptor

What is Raptor?

raptor logo

Raptor1 is a modern, fast, and easy-to-use system for building disk images, bootable isos, containers and much more - all from a simple, Dockerfile-inspired syntax.

It uses systemd-nspawn for sandboxing when building or running containers.

Eagle.. err, eager to get started?

Start by installing raptor, then head over to the Debian Liveboot walkthrough to get a hands-on introduction to building a bootable iso.

Theory of operation

Raptor builds layers from .rapt files. If you are familiar with Docker, this is similar to how Docker builds containers from a Dockerfile.

Raptor source code Raptor build Layers

However, Raptor has a different scope, and can do considerably different things than Docker.

Heads up!

The entire Raptor project, including this book, the program itself, and the companion project raptor-builders, is still quite young.

If you find (or suspect) any bugs, please report them so everybody can benefit.

At this point, Raptor has reached a stage where breaking changes are rare, but we don’t yet make any particular guarantees. We will try our best to announce major changes clearly, and ahead of time.

If you have questions, ideas or feedback, don’t hesitate to join the discussion.

Syntax

Raptor uses a syntax similar to Dockerfile. Statements start with uppercase keywords, and are terminated by end of line.

All lines starting with # are treated as comments:

# This copies "foo" from the host to "/bar" inside the build target
COPY foo /bar

Raptor files

Before being parsed as raptor files, .rapt files are processed through minijinja, a powerful templating language.


  1. According to wikipedia: “The word “raptor” refers to several groups of avian and non-avian dinosaurs which primarily capture and subdue/kill prey with their talons.“. Hopefully, this Raptor is less scary.

Install guide

Currently, the only supported and recommended way to install raptor is by compiling from source. Pre-built binary packages might be available in the future.

New user?

  1. Complete Install prerequisites
  2. Complete Install Raptor from git
  3. Enjoy Raptor 🦅

Install prerequisites

  1. Install necessary packages:

    sudo apt-get update
    sudo apt-get install -y git musl-tools
    
  2. Install Rust 1.90 or greater:

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
  3. Install musl target for rust:

    rustup target add x86_64-unknown-linux-musl
    

Now you are ready to install Raptor.

Install Raptor from git [stable]

  1. Install prerequisites

  2. Install raptor and falcon from git:

    cargo install \
        --target "x86_64-unknown-linux-musl" \
        --git "https://github.com/chrivers/raptor" \
        raptor falcon
    

Caution

The argument --target "x86_64-unknown-linux-musl" is very important!

Without it, Raptor will compile, but Falcon (the sandbox client used for building containers) will not be statically compiled. Since it is running inside sandboxed environments, where glibc is not always available in the correct version (or at all), you will get confusing errors when trying to build Raptor targets.

Consequence: without --target "x86_64-unknown-linux-musl", the cargo install command will compile and install, but the installation will be broken.

Install Raptor from git [development branch]

This procedure is intended for developers, beta testers, and anyone else who want to try a specific branch build of Raptor. To test a branch named hypothetical:

  1. Install prerequisites

  2. Install raptor and falcon from git:

    cargo install \
        --target "x86_64-unknown-linux-musl" \
        --git "https://github.com/chrivers/raptor" \
        --branch "hypothetical" \
        raptor falcon
    

Making a Debian liveboot iso with Raptor

In this walkthrough, we will take a look at all the steps needed to make a customized Debian liveboot .iso image.

By putting the image on a USB stick or external hard drive, any computer can be booted from this external device, without affecting existing data on the computer.

Using this technique, you will be able to create custom images, containing the tools, programs and settings that you like, and build them consistently and easily with raptor.

This guide is presented as a three-step process:

  • Build: Build raptor layers suitable for live booting.

  • Generate: Generate a live boot iso from the layers

  • Make: Use raptor-make to simplify the build process [optional]

TL;DR

Below is an absolutely minimal example of generating a liveboot iso, for the impatient.


Create a raptor file:

base.rapt

# Start from a docker iso
FROM docker://debian:trixie

# Set root password to "raptor"
RUN usermod -p "$1$GQf2tS9s$vu72NbrDtUcvvqnyAogrH0" root

# Update package sources, and install packages
RUN apt-get update
RUN apt-get install -qy systemd-sysv live-boot linux-image-amd64

Git clone (or otherwise download) the necessary build container recipes:

git clone https://github.com/chrivers/raptor-builders.git

Then use these to build liveboot.iso:

sudo raptor run -L rbuild raptor-builders '$rbuild.deblive' -I base -O liveboot.iso

After this command has run, you should have the file liveboot.iso available. Enjoy!

To learn how (and why) this works, read the next chapters of this guide.

Building a bootable target

In order to build a raptor target that can be turned into a bootable iso, there are a few requirements we need to consider:

  • The distribution must be Debian-based (or, preferably, Debian)
  • The package live-boot must be installed, since it contains the necessary tools and scripts
  • A linux kernel package must be installed (e.g. linux-image-amd64).

Here is an example of a fairly minimal base layer, which can be turned into an iso:

base.rapt

# Start from a docker iso
FROM docker://debian:trixie

# Set root password to "raptor"
RUN usermod -p "$1$GQf2tS9s$vu72NbrDtUcvvqnyAogrH0" root

# Update package sources, and install packages
RUN apt-get update
RUN apt-get install -qy systemd-sysv live-boot linux-image-amd64

The kernel package can take a bit of time to install, so let’s start a new layer for further customization. This way, we don’t need to rebuild the base layer with every change we make to the upper layer:

ssh.rapt

# Start from previous layer
FROM base

# Update package sources, and install ssh server
RUN apt-get update
RUN apt-get install -qy openssh-server

Of course, these layers could easily be combined, but it is good to get in a habit of separating things into reasonable layers. This improves caching and makes builds faster, since more can be reused between builds.

Now we are ready to build the layers. Since ssh derives from base (which derives from a docker layer), we just have to request raptor to build ssh. Raptor automatically determines dependencies, and builds them as needed.

sudo raptor build ssh

You should now see raptor build the ssh layer, with all command output from the apt-get commands being shown in the terminal.

Once complete, you can quickly verify that the layer is complete, by running the same command again. This time it should very quickly display a message, indicating that each layer is complete:

$ sudo raptor build ssh
[*] Completed [675DE2C3A4D8CD82] index.docker.io-library-debian-trixie
[*] Completed [AB1DD71BFD07718B] base
[*] Completed [80E4F4E5B0E2F6BA] ssh

In the next chapter we will see how we can turn these layers into a bootable iso file.

Generating an iso file

In the last chapter we built some raptor layers that serve as the file content for the bootable iso we want to build.

These layers are very similar to layers in a docker file1.

After building ssh (which implies the base target), their respective contents can be found in layers/....

Once built like this, it’s possible to “run” the container, like so:

$ sudo raptor run ssh id
[*] Completed [675DE2C3A4D8CD82] index.docker.io-library-debian-trixie
[*] Completed [AB1DD71BFD07718B] base
[*] Completed [80E4F4E5B0E2F6BA] ssh
uid=0(root) gid=0(root) groups=0(root)

Instead of id, an interactive command like sh or bash could be specified. This is very similar to how docker run works, except raptor containers are ephemeral by default. If no further options are specified, any changes made to the container will only be saved while the container is running.

This is all well and good, but we are interested in building a bootable iso from the layers, not just running them as a container.

It turns out that having a stack of layers available, is quite a powerful primitive. With the right commands, we could build all kinds of output from them; a .tar.gz file with a release archive, a bootable iso, or even a disk image for a machine.

In other words, the same input layers can be post-processed into a wide variety of output layers, depending on what you need.

To help get started with this, Raptor provides a standard set of build containers, in the companion project raptor-builders:

  • [deblive] Builds Debian liveboot isos from Raptor layers.
  • [disk-image] Builds disk images (for virtual or physical machines) from Raptor layers.

Build containers

Raptor build containers are just regular Raptor containers, that are configured to expect an input, and produce an output.

The containers from the raptor-builders project are available to anyone, but are not “built in” to Raptor - they don’t use any special features or private APIs. Anyone can make build containers that are similar (or even identical to) raptor-builders.

Since this walkthrough is focused on making a liveboot iso, we’ll use the deblive builder to convert layers to an iso file.

First, we need to clone (or otherwise download) the raptor-builders project:

git clone https://github.com/chrivers/raptor-builders

This builder uses mounts to access input, output and cache from outside the container (see MOUNT).

To build the layers from the last step into a debian liveboot iso, use the following command (assuming raptor-builders is checked out next to the directory containing ssh.rapt):

sudo raptor run                \
  -L book   book/example       \
  -L rbuild ../raptor-builders \
  -C liveboot-cache            \
  -I '$book.ssh'               \
  -O custom-liveboot.iso       \
  '$rbuild.deblive'

Tip

The command above uses the short-form command line options typically used interactively. Long-form options are also available, if greater clarity is desired (e.g. for scripting purposes).

sudo raptor run                    \
  --link book   book/example       \
  --link rbuild ../raptor-builders \
  --cache liveboot-cache           \
  --input '$book.ssh'              \
  --output custom-liveboot.iso     \
  '$rbuild.deblive'

(See the section on module names to learn more about --link and the $rbuild notation.)

This build process works, but the required command is fairly long and complicated.

In the next chapter, we’ll take a look at raptor-make, and how it can simplify the process.


  1. There’s a key difference between how Raptor and Docker handles layers. In Docker, each command (RUN, COPY, etc) creates a new layer. In Raptor, each .rapt file forms a new layer.

Make

In the previous chapter we took a look at the final command used to build a debian liveboot iso:

sudo raptor run                    \
  --link book   book/example       \
  --link rbuild ../raptor-builders \
  --cache liveboot-cache           \
  --input '$book.ssh'              \
  --output custom-liveboot.iso     \
  '$rbuild.deblive'

As mentioned, this works, but it doesn’t exactly roll off the tongue.

Having to type this command every time we want to build the iso, is not a satisfying solution.

To solve this, the subcommand raptor make is used. It is a make-like system, where dependencies are specified in a configuration file.

For make, this is a Makefile, for Rust it’s Cargo.toml, and for raptor make we have Raptor.toml.

Building a Raptor.toml for your project is recommend, but not required. It is perfectly possible to start using raptor from the command line, and only write a Raptor.toml file when it feels right.

That being said, the format for Raptor.toml has been designed to be a smooth transition from a constructed command line. The smallest valid Raptor.toml file is an empty one.

Let’s start by adding the two linked packages (--link arguments):

Raptor.toml

[raptor.link]
book = "book/example"
rbuild = "../raptor-builders"

We have added two package links, but of course any number can be added as desired.

All sections named [raptor.*] are settings for Raptor. The section [raptor.link] is for specifying linked packages.

Next, we will define a run job, which accounts for almost all of the remaining command line arguments.

A Raptor.toml file can have any number of [run.*] sections, each with their own name. Each of those sections is a separate run job. Let’s use book-ssh for this example:

Raptor.toml

[raptor.link]
book = "book/example"
rbuild = "../raptor-builders"

# The name `book-ssh` is not special.
# Feel free to choose any name you like!
[run.book-ssh]
target = "$rbuild.deblive"
cache = "liveboot-cache"
input = "$book.ssh"
output = "custom-liveboot.iso"

Now that we have encoded all the arguments, we can use a much simpler raptor make command to run this build job:

sudo raptor make book-ssh

Module names

In Raptor, “module names” are the names Raptor files use to refer to other Raptor files.

This is the case both for the FROM instruction (where the modules have the .rapt extension) and INCLUDE (.rinc extension).

There are three types of module names; Relative, Absolute and Package names.

TypeExampleDescription
Relativefoo.barUsed to refer to paths at or below the current directory
Absolute$.foo.barUsed to refer to paths from the root of the current package
Package$foo.barUsed to refer to paths in other packages

The difference between these forms is how they resolve to paths in the filesystem. For a detailed explanation of each, see the following sections.

Instancing

Each of these forms support instancing, which is a way to pass a single, simple parameter through the module name, by appending a @ followed by the value, to the module name.

All raptor files are either instanced (example@.rapt / example@.rinc) or not (example.rapt / example.rinc).

Non-instanced files cannot be referenced with an instanced name, and vice versa.

Users of systemd might recognize this pattern, which is used in the same way there.

To learn more, read the section on instancing.

Learn more

Each type of module name is described in more detail:

Relative module names

The first, and arguably simplest form, is the Relative name. It is characterized by not having a dollar sign ($) in front of the first element, unlike the other two forms.

Relative module paths form a sequence of one or more elements, which are the names between the dots (.).

Each element before the last is viewed as a directory. The last element is the file name, appended with either .rapt for FROM instructions, or .rinc for INCLUDE instructions.

In other words, a.b.c.d becomes a/b/c/d.rapt (FROM) or a/b/c/d.rinc (INCLUDE).

Examples:

StatementSource fileResulting path
FROM basetarget.raptbase.rapt
FROM baselib/target.raptlib/base.rapt
FROM utils.baselib/target.raptlib/utils/base.rapt
INCLUDE babelfishlib/target.raptlib/babelfish.rinc
INCLUDE hitchhiker.guidelib/target.raptlib/hitchhiker/guide.rinc

Absolute module names

Relative module names are simple, but lack the ability to point upwards in the directory heirarchy.

For example, suppose you want to organize a set of Raptor files like so:

~/project/base.rapt
~/project/hostname.rinc
~/project/target/common.rapt
~/project/target/frontend.rapt
~/project/target/database.rapt
~/project/lib/utils/tools.rinc

The base layer can INCLUDE hostname, but the frontend and database targets have no way to specify they want to build FROM the base layer!

This is where absolute module names are useful.

By prefixing a name with $. (dollar + dot) the name refers to the root of the current package.

We will go into more detail of what a package is, in the next section. For now, it is enough to know that when invoking raptor on a target, the root for that target is the directory that raptor was started from.

StatementSource fileResulting path
FROM $.basetarget/frontend.raptbase.rapt
FROM commontarget/frontend.rapttarget/common.rapt
FROM $.target.commontarget/frontend.rapttarget/common.rapt
INCLUDE $.lib.utils.toolstarget/database.raptlib/utils/tools.rapt

Package module names

In the previous section, we briefly mentioned packages.

Raptor is designed to work collaboratively, encouraging sharing of code between projects, and people. A Raptor package is simply a collection of useful Raptor files, typically distributed as a git repository.

This means we need a robust way to refer to Raptor files outside of our current project, as well as a way to tell Raptor how to find these files.

This is where package module names are used.

First, let us take a look at how to make Raptor aware of external code bases. This is called linking, analogous to how the term is used when building a program from several sources.

When invoking raptor, the -L option defines a linked raptor package:

sudo raptor -L name src ...

For example, imagine we are working on a web service, and we want to generate the deployment with Raptor. Now imagine this requires a database server, and that someone else is responsible for the Raptor code that controls the database.

Then we might have a file layout like so:

~/project/web/server.rapt
~/project/database/db-setup.rinc

We fully control ~/projects/web, but ~/project/database is a git repository we only pull changes from.

We would like to INCLUDE the db-setup module, but it exists outside our own repository.

This can be solved by declaring database as linked package:

$ cd ~/project/web
$ sudo raptor build server -L database ../database

Now, we can refer to the content of ../database by using $database.

Tip

We don’t have to give the link the same name as the directory it refers to!

If we link with -L lib1 ../database, we could instead refer to it as $lib1.db-setup.

Feel free to use any link name that suits the project; the linked names have no impact outside your project.

Instancing

Raptor files can be instanced, which makes them work as a template.

Instanced files can be recognized by ending in @.rapt (for FROM) or @.rinc (for INCLUDE).

File names and syntax

FileInstanced?Example
base.raptNoFROM base
server@.raptYesFROM server@production
settings.rincNoINCLUDE settings
firewall@.rincYesINCLUDE firewall@full

The table above shows some examples of instanced and non-instanced Raptor files.

Beware

It is invalid to reference an instanced file without an instance.

For example, FROM server@ or FROM base@value would both fail to compile.

Therefore, when writing a new Raptor file, you need to determine if it needs to be instanced, and name the file accordingly.

Using instancing

So far, we have seen how to create templated (instanced) Raptor files, and how to reference them to provide a value.

Now we will see how to use the provided value, so that instancing becomes useful. Let us start the simplest possible example

build-stamp@.rinc

WRITE "We are instance {{instance}}\n" /root/build-stamp.txt

This writes a human-readable line of text to a file, including the instance id.

Now we can use it from another file:

server.rapt

# Will write "We are instance 1\n" to /root/build-stamp.txt
INCLUDE build-stamp@1

Strings

In raptor files, string values can be expressed as quoted strings, or in certain cases as so-called barewords:

RUN echo these are barewords

RUN "echo" "these" "are" "quoted" "string"

For example, the following two statements are equivalent:

RUN echo word1 word2

RUN echo "word1" "word2"

But the following two statements are not:

# This creates 1 file called "filename with spaces"
RUN touch "filename with spaces"

# This creates *3 files* called "filename", "with", and "spaces", respectively
RUN touch filename with spaces

Tip

Think of barewords as a convenience, to avoid needing to quote everything all the time.

It is always valid and safe to use quoted strings to clearly convey the intended meaning.

When in doubt, use quotes.

String escaping

When using a quoted string, the backslash character (\) gains special meaning, and is known as the escape character.

When it is followed by certain other characters, the combined expression is replaced in the string:

Escape expressionResult
\\A single literal backslash
\nNewline (as if the string continued on the next line of text)
\tTabulator (useful to make tab clearly visible, and copy-paste proof)
\"Quote character (as opposed to ending the string)

A backslash followed by any other character will result in a parse error.

Important

Because backslash (\) is used as the escape character in quoted strings, any literal backslashes must themselves be escaped, by adding another backslash (\\).

Expressions

Raptor supports a limited form of inline literal expressions (i.e. “values”), that can be specified when using the INCLUDE and RENDER instructions.

Booleans

Boolean values work identically to json (and many other languages):

LiteralValue
truetrue
falsefalse

Integers

Integer values are supported.

Be aware that raptor always uses i64 (signed 64-bit) integers.

This is unlike the typical json implementations, that uses f64 (64-bit floating-point) numbers, in two important ways:

A) The valid range for i64 integers is -9223372036854775808 to 9223372036854775807, inclusive. This should be sufficient for most applications. Any integer in this range will be represented exactly.

B) Floating-point (i.e. fractional) numbers are not supported. For example, 3.14 is not valid in raptor. Instead, consider passing such a value as a string.

Currently, alternate integer bases (i.e. hexadecimal or octal) are not supported.

Strings

String are supported, and work much like they do in json, or other common notations.

Tip

See the section on string escapes for more details.

Lists

Lists are supported, with a syntax very similar to json. The only difference is that raptor allows an optional trailing comma after the last list element, while json does not.

Examples of lists

[1, 2, 3]
[true, false]
[["a", "b"], 123]

Maps

Maps (also typically known as dicts or hashmaps) contain a set of (key, value) pairs.

The syntax is similar to json. Like lists, raptor allows an optional trailing comma after the last key-value pair.

Examples of maps

{"answer": 42}
{"name": "Heart of Gold", "engine": "Improbability drive"}

File options

Several instructions (COPY, WRITE, RENDER, MKDIR) write files into the build target. They all supports common options that affect how the files are written.

Important

File options must be specified after the instruction (i.e. before source or destination path)

Change mode: --chmod <mode>

The --chmod option specifies the mode bits (i.e. permissions) associated with the file. The mode is specified as 3 or 4 octal digits.

Examples:

# these are equivalent:
COPY --chmod  755 script.sh /root/script.sh
COPY --chmod 0755 script.sh /root/script.sh

# set suid bit:
COPY --chmod 4755 sudo /usr/bin/sudo

# user access only, read-only:
WRITE --chmod 0400 "secret" /etc/service/token

Tip

The 3-digit version is identical to the 4-digit version, where the first digit is zero (which is a common case). For example, 755 and 0755 represent the same permissions.

Change owner: --chown <owner>

The --chown option specifies the user and/or group to own the file.

InputUserGroup
useruser(no change)
:(no change)(no change)
:group(no change)group
user:groupusergroup
user:useruser (!)

Important

Notice the last form, where user: is shorthand for user:user.

This is the same convention used by GNU coreutils, and several other programs.

Create parent directories: -p

Note

This file option is only valid for the MKDIR instruction.

The -p option instructs MKDIR to create parent directories as needed.

Importantly, it also makes MKDIR accept existing directories, including the last directory.

This is identical to the behavior of -p with the shell command mkdir.

# will fail if:
#   A) /foo is missing
# or:
#   B) /foo/bar already exists
MKDIR /foo/bar

# this will create:
#   /foo (if missing)
# and then
#   /foo/bar (if missing)
MKDIR -p /foo/bar

Mount types

Tip

For an introduction to mounts, see the MOUNT referrence.

When running containers, Raptor supports mounting various file resources into the container namespace.

In the simplest form, this means presenting a directory from the host environment, at a specified location in the container.

This is equivalent to how Docker uses volume mounts (-v / --volume).

However, Raptor supports more than just mounting directories:

TypeDescriptionAccess
MOUNT --simple ...Mounts a directory from the host (default)Read/write
MOUNT --file ...Mounts a single file from the hostRead/write
MOUNT --layers ...Mounts a view of set of layers as directoriesRead-only
MOUNT --overlay ...Mounts a view of the sum of a set of layersRead-only

Tip

If no mount type is specified, --simple is implied as the default.

For clarity, it is recommended to always specify a mount type.

Read more about the specific mount types:

Mount type --simple

This is the default mount type.

A --simple mount will mount a directory from the host into the container. Docker users are likely to be familiar with this concept.

Example

file-lister.rapt

FROM docker://debian:trixie

MOUNT --simple list /input

CMD "ls -l /input"

This container can be run, to provide a file listing on the mounted directory:

$ sudo raptor run file-lister -M list /tmp
... <"ls" output would be shown here> ...

Mount type --file

This mount type requires that a single file is mounted.

Tip

When running a raptor container with a --file mount, the target file will be created if it does not exist.

Example

Let us expand on the earlier example, to make the file lister provide output to a file.

file-lister-output.rapt

FROM docker://debian:trixie

MOUNT --simple input /input
MOUNT --file output /output

CMD "ls -l /input > /output"

Now that we have named the mounts input and output, we can use the shorthand notation for convenience:

$ sudo raptor run file-lister-output -I /etc -O /tmp/filelist.txt
...
$ sudo cat /tmp/filelist.txt
... <"ls" output would be shown here> ...

The above example would generate a file listing of /etc from the host, and place it in /tmp/filelist.txt. However, the execution of ls takes place in the container.

Mount type --layers

The --simple and --file mount types both present content from the host filesystem inside the container, and both have equivalents in Docker.

This is not the case for --layers, which is specific to Raptor.

When using a --layers mount, the input argument when running Raptor is not a file path, but a set of Raptor build targets.

Let us take a look at a target, that lists the files in a --layers mounts:

layers-lister.rapt

FROM docker://debian:trixie

MOUNT --layers input /input

CMD "ls -l /input"

Let’s try running this, using the previous file-lister.rapt target as input:

$ sudo raptor run layers-lister -I file-lister
total 12
drwxr-xr-x  2 root root 4096 Oct 17 13:37 file-lister-16689594BA5D2989
drwxr-xr-x 17 root root 4096 Sep 29 00:00 index.docker.io-library-debian-trixie-675DE2C3A4D8CD82
-rw-r--r--  1 root root  200 Oct 17 13:37 raptor.json

We see that each layer in the input is available as a directory, but also a raptor.json file.

Think of this file as a manifest of the contents in the --layers mount. It contains useful metadata about the inputs, including which targets have been specified, as well as the stacking order for each target:

raptor.json

{
  "targets": [
    "file-lister"
  ],
  "layers": {
    "file-lister": [
      "index.docker.io-library-debian-trixie-675DE2C3A4D8CD82",
      "file-lister-16689594BA5D2989"
    ]
  }
}

At first, this might seem like overly complicated. If we just needed the layer order, surely a simple text file would suffice?

In fact, multiple build targets can be specified at the same time:

$ sudo raptor run layers-lister -I file-lister -I file-lister-output
total 16
drwxr-xr-x  2 root root 4096 Oct 17 11:33 file-lister-16689594BA5D2989
drwxr-xr-x  2 root root 4096 Oct 17 11:31 file-lister-output-2CFDE4FEBD507157
drwxr-xr-x 17 root root 4096 Sep 29 00:00 index.docker.io-library-debian-trixie-675DE2C3A4D8CD82
-rw-r--r--  1 root root  381 Oct 17 11:45 raptor.json

We now have multiple build targets, and the Docker layer is shared between both inputs.

However, we can still make sense of this using raptor.json:

raptor.json

{
  "targets": [
    "file-lister",
    "file-lister-output"
  ],
  "layers": {
    "file-lister-output": [
      "index.docker.io-library-debian-trixie-675DE2C3A4D8CD82",
      "file-lister-output-2CFDE4FEBD507157"
    ],
    "file-lister": [
      "index.docker.io-library-debian-trixie-675DE2C3A4D8CD82",
      "file-lister-16689594BA5D2989"
    ]
  }
}

This mount type is useful for any build container that needs to work the contents of individual layers.

For example, the deblive builder1 uses this mount type, in order to build Debian liveboot images.


  1. From the raptor-builders companion project.

Mount type --overlay

Like --layers, this is a Raptor-specific mount type.

Raptor targets are almost always built from a set of layers (i.e., there’s at least one FROM instruction).

When running a raptor container, this stack of layers is combined using overlayfs, which makes the kernel present them to the container as a single, unified filesystem.

For build containers that need to operate on this combined view of a build target, the --overlay mount type is available.

For example, the disk-image builder[^deblive] uses this mount type, in order to build disk images for virtual (or physical) machines.

overlay-lister.rapt

FROM docker://debian:trixie

MOUNT --overlay input /input

CMD "ls -l /input"
$ sudo raptor run overlay-lister -I file-lister
total 60
lrwxrwxrwx  1 root root    7 Aug 24 16:20 bin -> usr/bin
drwxr-xr-x  2 root root 4096 Aug 24 16:20 boot
drwxr-xr-x  2 root root 4096 Sep 29 00:00 dev
drwxr-xr-x 27 root root 4096 Sep 29 00:00 etc
drwxr-xr-x  2 root root 4096 Aug 24 16:20 home
lrwxrwxrwx  1 root root    7 Aug 24 16:20 lib -> usr/lib
lrwxrwxrwx  1 root root    9 Aug 24 16:20 lib64 -> usr/lib64
drwxr-xr-x  2 root root 4096 Sep 29 00:00 media
drwxr-xr-x  2 root root 4096 Sep 29 00:00 mnt
drwxr-xr-x  2 root root 4096 Sep 29 00:00 opt
drwxr-xr-x  2 root root 4096 Sep 29 00:00 proc
drwx------  2 root root 4096 Sep 29 00:00 root
drwxr-xr-x  3 root root 4096 Sep 29 00:00 run
lrwxrwxrwx  1 root root    8 Aug 24 16:20 sbin -> usr/sbin
drwxr-xr-x  2 root root 4096 Sep 29 00:00 srv
drwxr-xr-x  2 root root 4096 Aug 24 16:20 sys
drwxrwxrwt  2 root root 4096 Sep 29 00:00 tmp
drwxr-xr-x 12 root root 4096 Sep 29 00:00 usr
drwxr-xr-x 11 root root 4096 Sep 29 00:00 var

Raptor make

Overall structure

Below is an example of the overall structure of a Raptor.toml file.

Note: All parts are optional, so you only need to define the parts you need.

[raptor.link]
name1 = "path/to/source1"
name2 = "path/to/source2"
...

[run.purple]
# ..run target here..

[run.orange]
# ..run target here..

[group.green]
# ..group here..

[group.yellow]
# ..group here..

Run target format

A run target ([run.<name>]) is the most commonly used feature in Raptor.toml.

The structure for a job named example is shown below, where each field is specified with its default value.

Note: Only the target field is required! Everything else can be specified as needed.

Raptor.toml run

[run.example]
target = <required>

# Cache mounts
# (default is empty list)
#
# Note: A single element can be specified as a string instead of the list
cache = []

# Input mounts
# (default is empty list)
#
# Note: A single element can be specified as a string instead of the list
input = []

# Output mounts
# (default is empty list)
#
# Note: A single element can be specified as a string instead of the list
output = []

# Entrypoint arguments
entrypoint = []

# Command arguments
args = []

# BTreeMap<String, String>
env = {}

# State directory for container state
# (default is unset, meaning ephemeral containers)
#state_dir =

Group format

A group is used to collectively refer to a number of run and build jobs, by a single name.

[group.example]
# List of layers to build
#
# Default is empty list
build = []

# List of names for [run.<name>] targets to run
#
# Default is empty list
run = []

Grammar

Warning

The Raptor parser is implemented by hand in a separate crate.

Because it is hand-written, there is no exact BNF grammar that matches the parsing.

The following is an attempt to provide a detailed and realistic description of the accepted syntax, but might contain minor mistakes and inconsistencies.

The parsing starts from the first rule, <file>, and proceeds from there.

Syntax guide

SyntaxMeaning
rule?Match rule 0 or 1 times (i.e., it is optional)
rule+Match rule 1 or more times
rule*Match rule 0 or more times
rule1 | rule2Match either rule1 or rule2 (exactly one of them must match)
( rule1 rule2 .. )Parenthesized rules are matched/optional/repeated together
"word"Matches the letters w, o, r, d (but not the quotes)

Raptor grammar

<file>         ::= <statement>*

<statement>    ::= <from>
                 | <mount>
                 | <render>
                 | <write>
                 | <mkdir>
                 | <copy>
                 | <include>
                 | <run>
                 | <env>
                 | <workdir>
                 | <entrypoint>
                 | <cmd>

<from>         ::= "FROM" <from-source> "\n"
<mount>        ::= "MOUNT" <mount-type>? <word> <path> "\n"
<render>       ::= "RENDER" <file-option>* <path> <path> <include-arg>* "\n"
<write>        ::= "WRITE" <file-option>* <value> <path> "\n"
<mkdir>        ::= "MKDIR" <mkdir-option>* <path> "\n"
<copy>         ::= "COPY" <file-option>* <path>+ <path> "\n"
<include>      ::= "INCLUDE" <module-name> <include-arg>* "\n"
<run>          ::= "RUN" <word>+ "\n"
<env>          ::= "ENV" <env-assign>+ "\n"
<workdir>      ::= "WORKDIR" <path> "\n"
<entrypoint>   ::= "ENTRYPOINT" <word>* "\n"
<cmd>          ::= "CMD" <word>* "\n"

<env-assign>   ::= <word> ( "=" <value> )?
<mount-type>   ::= "--file" | "--simple" | "--layers" | "--overlay"

<mkdir-option> ::= <file-option> | "-p"
<file-option>  ::= <file-chown> | <file-chmod>
<file-chown>   ::= "--chown" "="? <chown>
<file-chmod>   ::= "--chmod" "="? <chmod>
<chown>        ::= (<word> (":" <word>?)?) | (":" <word>?)
<chmod>        ::= /* built-in rule: 3 or 4 octal digits */

<include-arg>  ::= <word> ( "=" <expression> )?
<expression>   ::= <expr-lookup> | <expr-value>
<expr-lookup>  ::= <word> ("." <word>)*
<expr-value>   ::= <expr-list>
                 | <expr-map>
                 | <expr-string>
                 | <expr-number>
                 | <expr-bool>
<expr-list>    ::= "[" ( <expr-value>   ( "," <expr-value>   )* ","? )? "]"
<expr-map>     ::= "{" ( <expr-mapitem> ( "," <expr-mapitem> )* ","? )? "}"
<expr-mapitem> ::= <expr-value> ":" <expr-value>
<expr-string>  ::= /* built-in rule: see section on string escapes */
<expr-number>  ::= <digit>+
<expr-bool>    ::= "true" | "false"
<digit>        ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"

<module-name>  ::= <mn-package>? <mn-body> <mn-instance>?
<mn-package>   ::= "$" <word>? "."
<mn-body>      ::= <word> ( "." <word> )*
<mn-instance>  ::= "@" <word>
InstructionMultiple valid?Build or Run
FROMNoBuild
RUNYesBuild
ENVYesBuild
WORKDIRYesBuild
WRITEYesBuild
MKDIRYesBuild
COPYYesBuild
INCLUDEYesBuild
RENDERYesBuild
MOUNTYesRun
ENTRYPOINTNoRun
CMDNoRun

Instruction: FROM

Summary

FROM [<schema>://]<from-source>

The FROM instruction bases the current layer on top of some the specified layer.

This instruction is not required. If no FROM instruction is used, the target is building from an empty base, with no dependencies.

Multiple FROM instructions are not supported.

From sources

Raptor supports multiple options for from-source:

TypeSchemaExample
Raptor<none>FROM library.base
Dockerdocker://FROM docker://debian:trixie

Raptor sources

When no schema is specified, the from-source is assumed to be the module name of another raptor layer.

Tip

This will be familiar to docker users. For example..

# Dockerfile
FROM base

..will depend on the docker image base

However, unlike docker files, raptor can point to raptor files in other directories, or even other packages. See module names for an overview.

Examples

# This will depend on `base.rapt`
FROM base
# This will depend on `library/debian.rapt`
FROM library.debian

Docker sources

To use a docker image as the basis for a raptor layer, specify the name of the docker image, prefixed with docker://, e.g:

FROM docker://debian:trixie

Tip

In general, docker pull <NAME> becomes FROM docker://<NAME>

There are multiple (optional) parts in a docker reference, which has a surprisingly intricate syntax.

Raptor supports the entire grammar for docker references, so anything that docker pull will accept, should work with FROM docker:// in raptor.

Instruction RUN

Summary

RUN <command> [...<arg>]

The RUN instruction executes the given command inside the build namespace.

# enable the foo service
RUN systemctl enable foo.service

Important

Arguments are executed as-is, i.e. without shell expansion, redirection, piping, etc.

This ensures full control over the parsing of commands, but it also means normal shell syntax is not available:

# BROKEN: This will call "cat" with 3 arguments
RUN cat /etc/hostname "|" md5sum
#   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this will not work

Instead, /bin/sh can be called explicitly:

# This will produce the md5sum of /etc/hostname
RUN /bin/sh -c "cat /etc/hostname | md5sum"

Instruction ENV

Summary

ENV <key>=<value> [...<key=value>]

The ENV instruction sets one or more environment variables inside the build namespace.

Important

The ENV instructions only affects building a container, not running a container.

Example:

ENV CFLAGS="--with-sprinkles"
ENV API_TOKEN="acbd18db4cc2f85cedef654fccc4a4d8" API_USER="user@example.org"

The ENV instruction affects all instructions that come after it, in the same layer.

It is possible to overwrite a value that has been set previously:

FROM docker://busybox

ENV HITCHHIKER="Arthur Dent"
RUN sh -c "echo $HITCHHIKER" # outputs "Arthur Dent"

ENV HITCHHIKER="Ford Prefect"
RUN sh -c "echo $HITCHHIKER" # outputs "Ford Prefect"

Instruction WORKDIR

Summary

WORKDIR <path>

Important

The WORKDIR instructions only affects building a container, not running a container.

The WORKDIR instruction changes the current working directory inside the build namespace. This affects all subsequent relative paths, including the RUN instruction.

The workdir is not inherited through FROM.

The initial workdir is always /.

Example

# This will copy "program" to "/bin/program" (since initial directory is "/")
COPY program bin/program

# This creates /foo
RUN /bin/sh -c "touch foo"



# Switch to /usr
WORKDIR /usr

# The same command will now copy "program" to "/usr/bin/program"
COPY program bin/program



# Switch to /tmp
WORKDIR /tmp

# This creates /tmp/foo
RUN /bin/sh -c "touch foo"

Instruction WRITE

Summary

WRITE [<file-options>] <value> <path>

Tip

See the section on file options.

The WRITE instruction writes a fixed string to the given path.

A file can be added to the build output with COPY, but sometimes we just need to write a short value, and COPY might feel like overkill.

Using WRITE, we can put values directly into files:

WRITE "hello world" hello.txt

Tip

Be aware that WRITE does not add a newline at the end of your input.

For text files, it is almost always preferred to end with a newline.

To do this, add \n at the end of the quoted string:

WRITE "hostname\n" /etc/hostname

The same file options as COPY and RENDER are accepted:

WRITE --chmod 0600 --chown service:root "API-TOKEN" /etc/service/token.conf

Instruction MKDIR

Summary

MKDIR [<file-options>] <path>

Tip

See the section on file options.

The MKDIR instruction creates an empty directory inside the build target.

This is roughly equivalent to the following command:

RUN mkdir /foo

However, using RUN mkdir requires the mkdir command to be available and executable inside the build target. This is not always the case, especially when building things from scratch.

Example

MKDIR /data

MKDIR -p --chown babelfish:daemon /usr/local/translate/

Instruction COPY

Summary

COPY [<file-options>] <source> [...<source>] <destination>

Tip

See the section on file options.

The COPY instruction takes one or more source files, and copies them to the destination.

If multiple source files are specified, the destination MUST BE a directory.

InputDestinationResult
Single fileFileFile written with destination filename
Single fileDirectoryFile written to destination dir, with source filename
Multiple filesFileError
Multiple filesDirectoryFiles written to destination dir, with original filename
DirectoryAnyNot yet supported

Instruction INCLUDE

Summary

INCLUDE <module-name> [...<key>=<value>]

Tip

See the section on module names.

The INCLUDE instruction points to a Raptor include file (.rinc) to be executed. When using INCLUDE, any number of local variables can be passed to the included target.

For example, if we have previously made the file lib/install-utils.rinc that installs some useful programs, we can use that file in build targets:

# Note: We use module name notation when including files
#
# The file is called `lib/install-utils.rinc`, which makes
#    the module name `lib.install-utils`
INCLUDE lib.install-utils

We can also make the component accept parameters, to make powerful, flexible components:

# hypothetical library function to update "/etc/hostname"
INCLUDE lib.set-hostname hostname="server1"

In the above example, we set the hostname of a server using an included component.

Tip

Since all values have to be specified as key=value, we might end up passing variables through several raptor files. This often ends up looking like this in the middle:

INCLUDE setup-thing username=username password=password

This is of course valid, but a shorter syntax exists for this case:

INCLUDE setup-thing username password

In other words, an include parameter name=name can always be shortened to name.

Instruction RENDER

Summary

RENDER [<file-options>] <source> <destination> [...<include-arg>]

The RENDER instruction renders a file from a template, and writes it to the specified destination. It accepts the same key=value arguments as INCLUDE. These arguments are made available in the template.

Example:

RENDER widgetfactory.tmpl /etc/widgetd/server.conf host="example.org" port=1234

The short form name (meaning name=name) is also supported here.

For example, in a component where host and port are available in the environment:

RENDER widgetfactory.tmpl /etc/widgetd/server.conf host port

Instruction MOUNT

Summary

MOUNT [--<mount-type>] <name> <destination>

Build-time instruction

The MOUNT instructions only affects running a container, not building a container.

Tip

See the section on mount types.

By using MOUNT, targets can specify certain resources (files, directories, layers, etc) that should be made available when running the container.

Raptor mounts are identified by a name, which is used when running the container, to declare what to mount.

When running a raptor container, a mount input is specified with the -M <name> <source> command line option.

Tip

The syntax -M <input> <source> can be a bit unwieldy.

Since certain mount names are very common, they have a shorter syntax available:

NameLong formShort form
Input-M input <foo>-I <foo>
Output-M output <foo>-O <foo>
Cache-M cache <foo>-C <foo>

For example, suppose we have a simple container (disk-usage.rapt) that just calculates the disk space used by a mount:

FROM docker://debian:trixie

# Declare the mount "input", and place it at /input
MOUNT input /input

# Calculate the disk space used in /input
CMD "du -sh /input"

If we try to run this, we will get an error message:

$ sudo raptor run disk-usage
[*] Completed [675DE2C3A4D8CD82] index.docker.io-library-debian-trixie
[*] Completed [A24E97B01374CFEF] disk-usage
[E] Raptor failed: Required mount [input] not specified

As we can see, the container builds correctly, but fails because the input mount is not specified.

To fix this, we specify the input mount:

sudo raptor run disk-usage -I /tmp
[*] Completed [675DE2C3A4D8CD82] index.docker.io-library-debian-trixie
[*] Completed [4BDD0649E00CA728] disk-usage
128M    /input

We could have specified -I /tmp as -M input /tmp, but the short form usually makes the command easier to follow.

Mount type

The example have just looked will probably feel familiar to Docker users, since it is very similar to docker volumes, which are also bind mounts from the host to the container namespace.

However, Raptor mounts are more advanced than that.

For example, in order to build a Debian liveboot iso, we need to provide the Debian live-boot scripts with a set of squashfs files, generated from the individual layers of one or more raptor targets.

Docker does not easily provide access to container layers, as it is seen as more of an implementation detail.

In contrast, Raptor considers layers a first-class object, and makes them available using the MOUNT instruction.

To learn more about different mount types, please see the mount types section.

Instruction ENTRYPOINT

Build-time instruction

The MOUNT instructions only affects running a container, not building a container.

Instruction CMD

Build-time instruction

The MOUNT instructions only affects running a container, not building a container.