Skip to content

L-337 Service

ACTIVE DEVELOPMENT!

This is currently in active development. We encourage you trying it out. We discourage you relying on it.

The l337 service is the lowest level api: 4 service and it implements Lando Specification 337 which is so low level that it's too low to be the default service. That means you need to explicitly set type: l337 to use it.

In high level terms it combines service orchestration and image specification into a single file format.

Specifically, it is a light superset around the Docker Compose Version 3 format that also uses the Dockerfile specification.

This means that you should be able to paste Docker Compose and/or Dockerfile compatible contents into your Landofile, add api: 4 to each service and have it "just work".

As noted above Lando Specification 337 extends the Docker Compose 3 spec with Dockerfile stuff. That means that everything in both of those specs is supported by default. So this is a valid Landofile:

Landofile

yaml
name: "my-app"
services:
  # this is a Lando 3 php service
  php:
    type: "php:7.4"
    api: 3

  # these are Lando 4 "l337" services
  web:
    api: 4
    type: l337
    image: "nginx:1.15"
    networks:
      my-network:
    volumes:
      - "./:/app"
      - "my-data:/data"
  db:
    api: 4
    type: l337
    build:
      dockerfile: "./Dockerfile"
networks:
  my-network:
volumes:
  my-data:

OK cool, got it, but how does Dockerfile stuff factor in? What is actually different in the spec besides api?

Two questions.
One Answer: image

That's it.

Image

Lando Specification 337 is identical to the Docker Compose spec with the exception of the image key which now handles different string inputs and has an extended object format for MOAR POWAH.

The string input for image has been extended and now allows the below:

Landofile

yaml
name: "my-app"
services:
  # a valid registry image, eg the original Docker Compose usage
  example-1:
    api: 4
    type: l337
    image: "nginx:1.21"

  # a path to a Dockerfile compatible file
  example-2:
    api: 4
    type: l337
    image: "./images/nginx/Dockerfile"

  # raw Dockerfile compatible instructions
  example-3:
    api: 4
    type: l337
    image: |
      FROM nginx:1.21
      ENV HELLO there
      ...
      CMD run-stuff

You can extend image into object format to get access to more features. Here is an example that implements all the keys available in object format.

Landofile

yaml
name: "my-app"
services:
  example-1:
    api: 4
    type: l337
    image:
      imagefile: "nginx:1.21"
      tag: "pirog/nginx:1.21"
      buildkit: true
      ssh: false
      args:
        vibe: rising
      context:
        - "./nginx.conf:/etc/nginx/conf.d/default.conf"
      groups:
        - reallllllearly: -8675309
      steps:
        - instructions: "RUN id > /tmp/user"
          group: "reallllllearly"
        - instructions: |
            ENV VIBES rising
            RUN id > /var/ww/index.html
          group: "reallllllearly-10-before-root"

Imagefile

The image string notation format above actually populates the imagefile key behind the scenes. For that reason, this usage is the same as above but with a different key.

Use context instead of COPY/ADD

While you can use COPY and ADD instructions here we recommend you use context instead. See this for more information.

Landofile

yaml
name: "my-app"
services:
  # a valid registry image
  example-1:
    api: 4
    type: l337
    image:
      imagefile: nginx:1.21

  # a path to a Dockerfile compatible file
  example-2:
    api: 4
    type: l337
    image:
      imagefile: ./images/nginx/Dockerfile

  # raw Dockerfile compatible instructions
  example-3:
    api: 4
    type: l337
    image:
      imagefile: |
        FROM nginx:1.21
        ENV HELLO there
        ...
        CMD run-stuff

Note that you can also use dockerfile instead of imagefile if you prefer that usage. If you use both imagefile will win.

Build Args

You can pass in build args that are then used by your image instructions. Both array and object notation is permissible but object is probably better.

Landofile

yaml
name: "my-app"
services:
  build-args-1:
    api: 4
    type: l337
    image:
      args:
        - nginx=1.19.2
        - vibe=rising
        - something
      dockerfile: "./images/nginx/Vibefile"
  build-args-2:
    api: 4
    type: l337
    image:
      args:
        nginx: 1.21.5
        vibe: dialed
        something:
      imagefile: |
        ARG nginx=1.21.4
        FROM nginx:${nginx}
        ARG vibe
        ENV VIBE=${vibe}

Array formatted key=value pairs are trimmed in both key and value. Args with no value are ignored as in something above.

Buildkit

If for some reason you prefer to use the old pre-BuildKit image builder you can toggle this to false.

Landofile

yaml
name: "my-app"
services:
  # use the legacy builder
  example-1:
    api: 4
    buildkit: false
    type: l337
    image:
      imagefile: nginx:1.21

Note that if you do buildkit: false some image build features, specifically those in Dockerfile 1.1.0+ and the ssh features of this service, may not be available. You can also use buildx as an alias for buildkit. If you use both it will evaluate to true if either is true. Really just don't use both 😉!

You probably should just ignore this setting unless you have a well understood reason to do otherwise.

Context

If you would like to COPY and/or ADD files into your build context and image then use context. Many forms and options are supported:

Landofile

yaml
name: "my-app"
services:
  example-1:
    api: 4
    type: l337
    image:
      imagefile: "nginx:1.21"
      context:
        # COPY ./folder to /folder
        - "./folder"

        # COPY ./folder to /thing
        - "./folder:thing"

        # COPY file1 to /file2
        - "file1:/file2"

        # COPY file1 to /file3
        - src: "file1"
          dest: "file3"

        # COPY file1 to /file4
        - source: "file1"
          destination: "file4"

        # COPY ./images/Dockerfile to /images/Dockerfile
        - source: "./images/Dockerfile"

        # COPY file1 to /file6 and set ownership to nginx:nginx with perms 775
        - source: "file1"
          destination: "file6"
          owner: "nginx:nginx"
          permissions: "775"

        # COPY file1 to /file7 and set ownership to nginx:nginx
        - source: "file1"
          destination: "file7"
          user: "nginx"
          group: "nginx"

        # ADD HeresAHealthToTheCompany.json
        # to /SeaShanties/lyrics/main/shanties/HeresAHealthToTheCompany.json
        - source: "https://raw.githubusercontent.com/SeaShanties/lyrics/main/shanties/HeresAHealthToTheCompany.json"

        # ADD available-shanties.json
        # to /etc/config/available-shanties.json and set ownership to blackbeard
        - source: "https://raw.githubusercontent.com/SeaShanties/lyrics/main/available-shanties.json"
          dest: "/etc/config/available-shanties.json"
          owner: "eddie-teach"

Groups

groups allow you to organize steps.

By default every l337 service has two groups, default and context, with the following values:

yaml
context:
  description: "A group for adding and copying sources to the image"
  weight: 0
  user: "root"
default:
  description: "A default general purpose build group around which other groups can be added"
  weight: 1000
  user: "root"

You can add additional groups into your Landofile and then use them in your steps.

Landofile

yaml
name: "my-app"
services:
  example-1:
    api: 4
    type: l337
    image:
      imagefile: "nginx:1.21"
      groups:
        # adds a group called "val-jean" with weight "24601"
        # uses root user by default
        - val-jean: 24601

        # adds a root user group called "system" that runs first
        - name: "system"
          description: "Install system packages and stuff"
          weight: -10000
          user: "root"

        # adds a nginx user group called "user" that runs last
        - name: "user"
          description: "Allows for user run commands after other groups"
          weight: 10000
          user: "nginx"

SSH

If your image build requires access to ssh keys you have a few options:

Landofile

yaml
name: "my-app"
services:
  example-1:
    api: 4
    type: l337
    image:
      imagefile: Dockerfile
      # make all passphraseless keys and $SSH_AUTH_SOCK available
      ssh: true

  example-2:
    api: 4
    type: l337
    image:
      imagefile: Dockerfile
      # only make the keys available
      ssh:
        agent: false
        keys: true

  example-3:
    api: 4
    type: l337
    image:
      imagefile: Dockerfile
      # only make the agent available
      ssh:
        agent: true
        keys: false

  example-4:
    api: 4
    type: l337
    image:
      imagefile: Dockerfile
      # use particular agent and keys
      ssh:
        agent: /run/ssh/agent.pid
        keys:
          # can use a key
          - /home/.ssh/id_special
          # or load all the keys in a dir
          - /home/.ssh-more

  example-5:
    api: 4
    type: l337
    image:
      imagefile: Dockerfile
      # use particular agent via envar and a single key
      ssh:
        agent: $SSH_AUTH_SOCK_THING
        keys: /home/.ssh/id_rsa

As you can see its pretty configurable but we recommend that in most cases you just set ssh: true if you need ssh stuff as this is going to be your most portable option.

Here is a concrete example that uses a passphrase protected key that is first loaded into the ssh-agent and a few other passphrase-less keys that are in ~/.ssh.

Keys

~/.ssh/id_passphrased
~/.ssh/id_no_pp_1
~/.ssh/id_no_pp_2

Landofile

yaml
name: "my-app"
services:
  example-1:
    api: 4
    type: l337
    image:
      ssh: true
      imagefile: |
        FROM debian

        # will clone with $SSH_AUTH_SOCK which contains id_passphrased
        RUN --mount=type=ssh,id=agent git clone git@github.com:lando/lando.git /lando2

        # will clone with either id_no_pp_1 or id_no_pp_2
        RUN --mount=type=ssh,id=keys git clone git@github.com:lando/lando.git /lando

        # will clone with id_no_pp_1
        RUN --mount=type=ssh,id=id_no_pp_1 git clone git@github.com:lando/lando.git /lando3

Execution

sh
# start agent
eval "$(ssh-agent -s)"

# add passphrase protected key
ssh-add ~/.ssh/id_passphrased

# rebuild images
lando rebuild

Here are some additional notes on this feature:

  • Passphrase protected keys can be used but only if added to your host ssh-agent before image build
  • This feature is not available if you set buildkit: false
  • The keys/agent are only available when explicitly mounted in a RUN instruction they DO NOT end up in the resulting image

Steps

While you can specify an entire Imagefile's contents directly in image.imagefile it's often better to use steps which are ordered and partial Dockerfile-compatible instructions.

Because steps are wrapped in a standardized and ordered layer, Lando and any of its plugins can insert instructions into the resulting imagefile wherever they make the most sense. This allows for great flexibility.

However, we are mostly interested here in how steps can be used directly in a Landofile.

Landofile

yaml
name: "my-app"
services:
  example-1:
    api: 4
    type: l337
    image:
      imagefile: "nginx:1.21"
      groups:
        ... # as defined [above](#groups) and omitted here for brevity
      steps:
        # insert string instructions into the default group
        - instructions: |
            ENV VIBES RISING
            RUN apt-get update -y
            RUN /my-script.sh

        # insert array format instructions into the default group
        # See: https://www.npmjs.com/package/dockerfile-generator for syntax
        - instructions:
          - env:
              KIRK: "wesley"
              SPOCK: "peck"
          - run: "env"
          - run:
            - "stat"
            - "/tmp"

        # insert group detached, one-off, singleton, instructions
        # at arbitrary weight
        - instructions: "ENV PIKE mount"
          weight: 1001
        - instructions: |
            RUN echo "last" >> /stuff
          weight: 999
        - instructions: |
            RUN echo "first" >> /stuff
          weight: 1

        # insert instructions into groups
        - instructions: |
            ENV KIRK pine
            ENV SPOCK quinto
            RUN id > /system-user
          group: "system"
        - instructions: "RUN id > /tmp/user"
          group: "user"

        # insert instructions using group-override format
        # runs -4 weight units before the system group
        - instructions: |
            ENV KIRK shatner
            ENV SPOCK nimoy
          group: "system-4-before"
        # run 10 weight units after the user group but uses the root user
        - instructions: |
            ENV KIRK shatner
            ENV SPOCK nimoy
          group: "user-10-root"

Note that if you specify both a group and a weight the step will run at the weight regardless of the weight of the group. Generally it's a good idea to not use both group and weight in the same step.

If you reference a group that does not exist then the default group will be used.

COPY/ADD

While you can use COPY and ADD instructions here we recommend you use context instead. See this for more information.

Override syntax

The group override syntax is flexible as long as the parent group is first. For example the following overrides are equivalent:

bash
system-2
system-2-after
system-root-after-2
system-2-after-root
system-root-2-after

That said, we like the GROUP-OFFSET-DIRECTION-USER format. 😉

Also note that it is totally possible to have defined two groups like system and system-4. In this scenario a step using the system-4 group will actually use the system-4 group and not override the system group with a 4-after offset.

Since this can easily get confusing it's best to be careful when defining your group names.

Tag

If you wish to force Lando to use a particular tag on successful image creation, you can.

Landofile

yaml
name: "my-app"
services:
  tag-me-bro:
    api: 4
    type: l337
    image:
      imagefile: |
        FROM nginx:1.21
        ENV SERVER=apache
        ENV CONFUSED=true
      tag: "loki/apache:1.21"

Caveats

As you may have already suspected because the l337 service sits below the main lando service it lacks ALL the features in that service. Using it is pretty close to just using Docker Compose/Dockerfiles straight up.

Said another way, you should really only use this service directly if you are intentionally looking to avoid normal Lando features or want to use something that is more-or-less like Docker Compose.

That said, here are the things that make it pretty close but not identical to Docker Compose/Dockerfiles:

1. Auto app mount discovery

If you volume mount your app root directory then Lando will assume its mount destination as the app mount for tooling purposes. Consider the following example:

Landofile

yaml
name: "my-app"
services:
  my-service:
    api: 4
    type: l337
    image: "php:8.2-cli"
    volumes:
      - "./:/home"
tooling:
  pwd:
    service: "my-service"

Note that changing the directory on the host also changes the location at which lando pwd is executed within the container:

bash
lando pwd
# /home
cd subdir && lando pwd
# /home/subdir

2. Working dir support

If you do not mount your app root directory as above then Lando will use working_dir to set the default tooling dir for that service.

Landofile

yaml
name: "my-app"
services:
  my-service:
    api: 4
    type: l337
    image: "php:8.2-cli"
    working_dir: "/var/www"
tooling:
  pwd:
    service: "my-service"

Note that lando pwd still executes in the working_dir location, even though we change directories on the host machine:

bash
lando pwd
# /var/www
cd subdir && lando pwd
# /var/www

3. COPY & ADD considerations

While you can use the COPY and ADD instructions in various places in the l337 service we recommend you use our contextconvention instead. The tl;dr there is it will be more performant.

The full explanation is that if Lando detects a COPY or ADD instruction it will automatically copy the entire application root into the build context. So if you have a BIG APP this can drastically slow build performance.

If for some reason you need COPY/ADD to work eg you are basing your service on some preexisting Dockerfile with some build assets then we recommend you copy the build assets and Dockerfile into a subfolder and build from there. In this case only the files in that subfolder will be used and not the entire application.

Examples

If you would like to look at concrete and tested examples you can check out the below: