Write Dockerfiles and CI pipelines in TypeScript.
Trellis is a portable CI/CD tool. With Trellis, you can define your Dockerfiles and CI/CD pipelines in TypeScript, and run them anywhere (locally or on a hosted platform).
First, install Deno with brew install deno
(or comparable).
Second, install the Trellis CLI with:
deno install
--allow-run=docker
--allow-net
--allow-write
--allow-env
--allow-read
https://deno.land/x/[email protected]/cli.ts
Run trellis --help
to verify your installation:
>>> trellis --help
Usage: trellis build mod.ts
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
ls [file] List all Images and Tasks available in a
TypeScript module
preview [options] [file] Generate a Dockerfile defined in a TypeScript
module
build [options] [file] Build an Image defined in a TypeScript module
run [options] [file] Run a Task defined in a TypeScript module
help [command] display help for command
Export any Image
to enable Dockerfile generation and image building with
Trellis.
For example, to define an Ubuntu image with a few useful utilities installed,
you could write the following mod.ts
file:
import { Image } from "https://deno.land/x/[email protected]/mod.ts";
const UBUNTU_VERSION = "20.04";
export const buildStage = Image.from(`ubuntu:${UBUNTU_VERSION}`)
.workDir("/root")
.aptInstall([
"curl",
"jq",
"git",
]);
Running trellis ls mod.ts
lists the buildable Images:
>>> trellis ls mod.ts
Images:
- buildStage (trellis build --target buildStage)
We can preview the generated Dockerfile with
trellis preview mod.ts --target buildStage
:
>>> trellis preview --target buildStage
#syntax=docker/dockerfile:1.4
FROM ubuntu:20.04 AS stage-0
WORKDIR /root
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl git jq
We can build the image with trellis build --target buildStage
:
>>> trellis build --target buildStage
[+] Building 0.6s (11/11) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 335B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1.4 0.2s
=> CACHED docker-image://docker.io/docker/dockerfile:1.4@sha256:9ba7531bd80fb0a858632727cf7a112fbf 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:20.04 0.2s
=> [stage-0 1/3] FROM docker.io/library/ubuntu:20.04@sha256:35ab2bf57814e9ff49e365efd5a5935b6915ee 0.0s
=> CACHED [stage-0 2/3] WORKDIR /root 0.0s
=> CACHED [stage-0 3/3] RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=c 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:17f750ba9a4becf38ce4d584d0de4793bfd6a8139674c3b332cdcdf6525ea8d9 0.0s
=> => naming to docker.io/trellis/db112e211de238c035a9fd3bbcbd5c417aafc5ee96a8c24d99d4caf81a759903 0.0s
√ Build: trellis/db112e211de238c035a9fd3bbcbd5c417aafc5ee96a8c24d99d4caf81a759903
Export any function from a TypeScript module to enable task execution with Trellis.
For example, to define a CI pipeline to verify that our command-line utilities
were successfully installed, you could write the following tasks.ts
file:
import { build, Image, run } from "https://deno.land/x/[email protected]/mod.ts";
import { buildStage } from "./mod.ts";
export default async function runChecks() {
await build(buildStage);
const checkCurl = Image.from(buildStage).run(
"curl --help",
);
const checkJq = Image.from(buildStage).run(
"jq --help",
);
const checkGit = Image.from(buildStage).run(
"git --help",
);
await Promise.all([
run(checkCurl),
run(checkJq),
run(checkGit),
]);
}
Running trellis ls tasks.ts
lists the executable Tasks:
>>> trellis ls tasks.ts
Tasks:
- default (trellis run tasks.ts)
We can execute the task locally with trellis run tasks.ts
:
>>> trellis run tasks.ts
[+] Building 1.1s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 335B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1.4 0.5s
=> [auth] docker/dockerfile:pull token for registry-1.docker.io 0.0s
=> CACHED docker-image://docker.io/docker/dockerfile:1.4@sha256:9ba7531bd80fb0a858632727cf7a112fbf 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:20.04 0.3s
=> [auth] library/ubuntu:pull token for registry-1.docker.io 0.0s
=> [stage-0 1/3] FROM docker.io/library/ubuntu:20.04@sha256:35ab2bf57814e9ff49e365efd5a5935b6915ee 0.0s
=> CACHED [stage-0 2/3] WORKDIR /root 0.0s
=> CACHED [stage-0 3/3] RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=c 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:17f750ba9a4becf38ce4d584d0de4793bfd6a8139674c3b332cdcdf6525ea8d9 0.0s
=> => naming to docker.io/trellis/adf8a603d1ab539848d89f68491e1b9213c1ca498f3f68d871e1b59c4c7de601 0.0s
√ Build: trellis/adf8a603d1ab539848d89f68491e1b9213c1ca498f3f68d871e1b59c4c7de601
√ Run: git --help
√ Run: jq --help
√ Run: curl --help
Trellis can be configured via a trellis.config.ts
file, the basic semantics of
which are modelled after Vite.
The trellis.config.ts
should contain a single default export consisting of a
defineConfig
invocation, like this:
import { defineConfig } from "https://deno.land/x/[email protected]/mod.ts";
export default defineConfig({
engine: "docker",
});
Trellis will use the closest trellis.config.ts
, looking first in the current
working directory, and then in each subsequent parent directory.
Trellis is compatible with depot.dev, which can be used to
enable cloud-accelerated builds with zero configuration. Run through the Depot
installation (brew install depot/tap/depot
or similar, followed by
depot login
), then define a trellis.config.ts
like so:
import { defineConfig } from "https://deno.land/x/[email protected]/mod.ts";
export default defineConfig({
engine: {
type: "depot",
project: "${YOUR_PROJECT_ID}",
},
});
From there, all Trellis builds will run through Depot.
Trellis runs on Deno, making it a one-step installation on GitHub Actions:
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
DOCKER_BUILDKIT: 1
jobs:
build:
name: "Build"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Install Deno"
uses: denoland/setup-deno@v1
with:
deno-version: "1.25.2"
- name: "Install Trellis"
working-directory: ./trellis
run: deno install --allow-run=docker --allow-net --allow-write --allow-env --allow-read https://deno.land/x/[email protected]/cli.ts
- name: "Build the image"
working-directory: ./examples/typescript
run: trellis build trellis/mod.ts
Trellis is motivated by the following observations, drawn from the experience of maintaining large, containerized CI/CD systems.
Dockerfiles are hard to maintain. Over time, large systems tend to accumulate collections of Dockerfiles with similar subsections, but no shared abstractions.
Efficient Dockerfiles are hard to write. Writing a Dockerfile that's
maximally cacheable, with a minimal footprint, requires significant
expertise. For example, to apt-get install
, the
Docker documentation
recommends the following:
RUN apt-get update && apt-get install -y
# Be sure to sort dependencies to maximize cacheability.
bzr
cvs
git
mercurial
subversion
# Clear the apt cache to minimize disk size.
&& rm -rf /var/lib/apt/lists/*
The CI/CD iteration loop is too slow. The common workflow for writing a new GitHub Actions pipeline, Jenkinsfile, etc., is to commit, push, wait for the system to acknowledge your change, then wait for your task to fail — tens, or even hundreds of times in a row. With existing CI solutions, you're writing code to run on an unfamiliar system, outside your control, without a first-class development workflow.
CI/CD systems create significant lock-in. Porting your Jenkinsfiles or YAML files to GitHub Actions, or vice versa, requires grappling with platform-specific abstractions.
Trellis solves these problems through a few significant design decisions.
First: With Trellis, you define your Dockerfiles and CI/CD pipelines in TypeScript. This gives you the power of a "full" programming language while retaining a declarative API. With TypeScript, we get the following benefits:
apt-get install
steps by hand.deno.land
.Second: Trellis makes local execution a first-class primitive. CI/CD
shouldn't feel like an entirely separate system; it should feel like running
code. Trellis is built on Deno and highly portable. You can run trellis build
locally just as you would on GitHub Actions or elsewhere. In this way, Trellis
takes inspiration from tools like Earthly and
Dagger.
Trellis has a few aspirational goals that aren't yet realized:
Trellis is both a library and a command-line interface. With Trellis, you export
Image
definitions and runnable functions (called "Tasks") from your TypeScript
modules, then execute them via the trellis
CLI.
trellis preview
Generate a Dockerfile defined in a TypeScript module.
Usage: trellis preview [options] [file]
Generate a Dockerfile defined in a TypeScript module
Options:
-t, --target <TARGET> Image to build within the TypeScript module
-h, --help display help for command
trellis build
Build an Image defined in a TypeScript module.
Usage: trellis build [options] [file]
Build an Image defined in a TypeScript module
Options:
-t, --target <TARGET> Image to build within the TypeScript module
--push Whether to push the image to a remote registry
-h, --help display help for command
trellis ls
List all Images and Tasks available in a TypeScript module.
Usage: trellis ls [options] [file]
List all Images and Tasks available in a TypeScript module
Options:
-h, --help display help for command
trellis run
Run a Task defined in a TypeScript module.
Run a Task defined in a TypeScript module
Options:
-t, --target <TARGET> Task to run within the TypeScript module
-h, --help display help for command
The ./examples
directory demonstrates a variety of use-cases for Trellis.
Trellis is flexible and can be used solely to generate Dockerfiles for other
systems, or for defining entire CI/CD pipelines.
rocket
: A Rust webserver atop the Rocket framework.
Demonstrates multi-stage builds and deployment via Fly.io
by leveraging trellis preview
.ruff
: A Rust command-line tool. Demonstrates efficient builds and CI checks.runc
: A Linux development container. Demonstrates generating artifacts with
Trellis and copying them back to the host machine.turborepo
: Turborepo's own Docker example, modified to generate Dockerfiles
with Trellis.typescript
: A TypeScript monorepo. Demonstrates efficient builds and CI
checks, along with consolidating constants (like the list of TypeScript
workspaces).wasm
: A "Hello, world!" Rust binary that's compiled to Wasm and tested on
Wasmtime.Trellis is built on Deno, which is distributed as a single binary executable
with no external dependencies. Using Deno means that installing Trellis
anywhere) is as simple as deno install ...
— there's no package.json
, no
npm install
, and no TypeScript transpilation step.
Similar to Nixpacks, Trellis generates Dockerfiles. This simplifies Trellis's implementation, but also enables users to leverage Trellis for Dockerfile generation alone, rather than as a complete CI/CD solution.
trellis build
and trellis run
depend on Docker and assume that the Docker
daemon is locally accessible.
MIT