Skip to content

Deploy container

In this tutorial, you'll preview and deploy a single container with syslet and check it runs under systemd. Then you'll change it, preview the change with --diff, apply it, and confirm re-applying an unchanged spec does nothing.

Prerequisites

1. Write a container spec

Create a file named webapp.json:

webapp.json
{
  "apiVersion": "v1",
  "type": "container",
  "name": "webapp",
  "desiredState": "running",
  "unit": {
    "Container": {
      "Image": "docker.io/library/nginx:latest",
      "PublishPort": ["8080:80"]
    }
  }
}

unit.Container maps directly onto the [Container] section of a quadlet .container file. syslet sets ContainerName to webapp, the spec's name.

2. Preview the deployment

Pipe the spec to syslet on the server over SSH, with --diff:

Bash
cat webapp.json | ssh web01 sudo syslet --stdin --diff

--diff runs the full plan (validate, diff against installed state) but stops before executing anything. It shows the unit file syslet would write, including the options it adds on its own, and that the container would be started:

Text Only
Images to pull:
  - docker.io/library/nginx:latest

Unit file changes:

--- webapp.container
added:
  [Container] ContainerName=webapp
  [Container] Image=docker.io/library/nginx:latest
  [Container] PublishPort=8080:80
  [Install] WantedBy=multi-user.target default.target
  [Service] Restart=always
  [Unit] Description=webapp container
  [X-Syslet] RemovalAllowed=true

Systemd daemon-reload: required

Containers to start:
  - webapp.container

Summary:
UNIT                                     STATUS     CHANGES
webapp.container                         created    created, image pulled, started (desired: running)

Nothing on the server is touched yet.

3. Apply it

Run the same command without --diff:

Bash
cat webapp.json | ssh web01 sudo syslet --stdin

syslet reads the spec, validates it, generates /etc/containers/systemd/webapp.container, runs systemctl daemon-reload, and starts the container. It logs each action as it runs, then prints the result per unit:

Text Only
1
2
3
4
5
time=2026-09-23T09:39:41.102+02:00 level=INFO msg="pulling image" image=docker.io/library/nginx:latest
time=2026-09-23T09:39:47.345+02:00 level=INFO msg="writing unit file" unit=webapp.container
time=2026-09-23T09:39:47.345+02:00 level=INFO msg=daemon-reload
time=2026-09-23T09:39:47.345+02:00 level=INFO msg=starting unit=webapp.container
webapp.container                         created    created, image pulled, started (desired: running)

It also saves the applied spec to /etc/syslet/config.json.

4. Verify

Bash
ssh web01 sudo systemctl status webapp.service
ssh web01 sudo podman ps

You should see webapp.service active and a running nginx container. Because desiredState: "running" was set, syslet also added Restart=always and WantedBy=multi-user.target default.target, so the container restarts on crash and starts on boot.

5. Preview a change

Edit webapp.json to publish an extra port:

webapp.json
{
  "apiVersion": "v1",
  "type": "container",
  "name": "webapp",
  "desiredState": "running",
  "unit": {
    "Container": {
      "Image": "docker.io/library/nginx:latest",
      "PublishPort": ["8080:80", "8443:443"]
    }
  }
}

Preview it again:

Bash
cat webapp.json | ssh web01 sudo syslet --stdin --diff

This time the plan diffs against the installed unit, so it only shows the new option, and that the container will be stopped and started again:

Text Only
Unit file changes:

--- webapp.container
added:
  [Container] PublishPort=8443:443

Services to stop:
  - webapp.container

Systemd daemon-reload: required

Containers to start:
  - webapp.container

Summary:
UNIT                                     STATUS     CHANGES
webapp.container                         updated    unit updated, restarted (desired: running)

Nothing on the server is touched yet.

6. Apply it

Bash
cat webapp.json | ssh web01 sudo syslet --stdin

syslet carries out the plan it just showed:

Text Only
1
2
3
4
5
time=2026-09-23T09:41:12.508+02:00 level=INFO msg=stopping unit=webapp.container
time=2026-09-23T09:41:12.508+02:00 level=INFO msg="writing unit file" unit=webapp.container
time=2026-09-23T09:41:12.508+02:00 level=INFO msg=daemon-reload
time=2026-09-23T09:41:12.508+02:00 level=INFO msg=starting unit=webapp.container
webapp.container                         updated    unit updated, restarted (desired: running)

7. Re-apply without changes

Run the exact same command again:

Bash
cat webapp.json | ssh web01 sudo syslet --stdin

syslet reports no changes and does not stop or restart webapp.service:

Text Only
No changes detected. All units are up to date.

Applying an unchanged spec is always a no-op, which makes it safe to re-run syslet on every deploy, or on a schedule, without unnecessary restarts.

See Deploy over SSH for more on this workflow. Continue with Setup CUE repository.