Laboratórios

Sessões curtas, do zero a um resultado que se verifica. Cada uma inclui a limpeza — nenhuma deixa nada para trás.

Short sessions, from zero to a verifiable result. Each one includes cleanup — none of them leaves anything behind.

A verificação no fim de cada lab não é decorativa: é o que separa «copiei comandos» de «sei o que isto faz». Onde um passo pode falhar em silêncio, o lab diz exactamente o que olhar.

The verification at the end of each lab isn't decorative: it's what tells apart "I copied commands" from "I know what this does." Where a step could fail silently, the lab tells you exactly what to look at.

Primeiro serviço, em 60 segundosFirst service, in 60 seconds iniciantebeginner

Objectivo: perceber o ciclo run → ps → logs → exec → rm e ver que não há daemon nenhum por baixo.

# 1. Um serviço web, em segundo plano, publicado na porta 8080 do host
delonix container run -d --name web -p 8080:80 nginx:alpine

# 2. Confirmar — o STATUS diz há quanto tempo está de pé
delonix container ps

# 3. A prova de que não há daemon: NENHUM processo residente do motor
pgrep -a delonix || echo "sem daemon — o container é filho do init do host"

# 4. Falar com ele
curl -s localhost:8080 | head -3

# 5. Entrar lá dentro
delonix container exec -it web sh -c 'hostname; id; ls /etc/nginx'

# 6. Ver o que ele escreveu
delonix container logs web | tail -5

# 7. Limpar
delonix container rm -f web

Verificação: o passo 3 não imprime processo nenhum do motor. Um container a correr sem supervisor residente é a diferença de fundo para o Docker, e vê-se aqui em duas linhas.

Goal: understand the run → ps → logs → exec → rm cycle and see that there's no daemon underneath.

# 1. A web service, in the background, published on host port 8080
delonix container run -d --name web -p 8080:80 nginx:alpine

# 2. Confirm — STATUS shows how long it's been up
delonix container ps

# 3. Proof there's no daemon: NO resident engine process
pgrep -a delonix || echo "no daemon — the container is a child of the host's init"

# 4. Talk to it
curl -s localhost:8080 | head -3

# 5. Go inside
delonix container exec -it web sh -c 'hostname; id; ls /etc/nginx'

# 6. See what it wrote
delonix container logs web | tail -5

# 7. Clean up
delonix container rm -f web

Verification: step 3 prints no engine process at all. A container running with no resident supervisor is the fundamental difference from Docker, and you can see it here in two lines.

Limites de recursos que pegam mesmoResource limits that actually take iniciantebeginner

Objectivo: descobrir se este host aplica limites — e o que fazer quando não aplica. É o erro nº1 de quem começa em rootless.

# 1. Perguntar ANTES de assumir
delonix system setup

# 2. Se disser INERT ou PARTIAL, a 1.ª correcção não precisa de root nenhum
systemd-run --user --scope -p Delegate=yes -- delonix system setup
#   … e é dentro desse scope que corres os passos seguintes.
#   Só se ELE ainda disser que falta o `cpu`:
#     sudo delonix system setup --delegate   (+ logout / login)

# 3. Um container com tecto de memória
delonix container run -d --name limitado -m 128M alpine sleep 300

# 4. A prova: ler o cgroup REAL, não o que o comando diz
PID=$(delonix container inspect limitado | jq -r .pid)
cat /sys/fs/cgroup$(awk -F: '/^0::/{print $3}' /proc/$PID/cgroup)/memory.max

delonix container rm -f limitado

Verificação: o passo 4 imprime 134217728 (128 MiB), não max. Se imprimir max, a delegação não está feita — e o container corre sem limite nenhum, em silêncio. É por isso que o passo 1 existe.

Nota: cpuset e io aparecem quase sempre como absent, e está certo — num Ubuntu de fábrica o user.slice (da root) só passa cpu memory pids para baixo. Nada aqui precisa deles.

Goal: find out whether this host enforces limits — and what to do when it doesn't. It's mistake #1 for anyone starting out in rootless.

# 1. Ask BEFORE assuming
delonix system setup

# 2. If it says INERT or PARTIAL, the 1st fix needs no root at all
systemd-run --user --scope -p Delegate=yes -- delonix system setup
#   … and you run the following steps INSIDE that scope.
#   Only if it STILL says `cpu` is missing:
#     sudo delonix system setup --delegate   (+ logout / login)

# 3. A container with a memory ceiling
delonix container run -d --name limited -m 128M alpine sleep 300

# 4. The proof: read the REAL cgroup, not what the command says
PID=$(delonix container inspect limited | jq -r .pid)
cat /sys/fs/cgroup$(awk -F: '/^0::/{print $3}' /proc/$PID/cgroup)/memory.max

delonix container rm -f limited

Verification: step 4 prints 134217728 (128 MiB), not max. If it prints max, delegation isn't set up — and the container runs with no limit at all, silently. That's why step 1 exists.

Note: cpuset and io almost always show up as absent, and that's correct — on a stock Ubuntu, root's user.slice only passes cpu memory pids down. Nothing here needs them.

Duas aplicações que se falam por nomeTwo apps that talk to each other by name intermédiointermediate

Objectivo: rede de utilizador, DNS interno, e isolamento — sem configurar DNS nenhum.

# 1. Uma rede própria
delonix network create loja

# 2. Base de dados e app, ambas nela
delonix container run -d --name db  --net loja -e POSTGRES_PASSWORD=x postgres:16-alpine
delonix container run -d --name app --net loja -p 8080:80 nginx:alpine

# 3. A app resolve a db PELO NOME, sem /etc/hosts nem variáveis
delonix container exec app sh -c 'getent hosts db; nc -z db 5432 && echo "porta aberta"'

# 4. Fechar a db a tudo menos à app (alcançabilidade DIRIGIDA)
cat > dep.yaml <<'EOF'
apiVersion: delonix.io/v1
kind: Dependency
metadata:
  name: app-conhece-db
spec:
  from: app
  to: db
  ports: ["5432"]
EOF
delonix stack apply -f dep.yaml

# 5. Provar: um terceiro container na MESMA rede já não alcança a db
delonix container run --rm --net loja alpine sh -c 'nc -z -w2 db 5432 || echo BLOQUEADO'

delonix container rm -f db app; delonix network rm loja; rm dep.yaml

Verificação: o passo 3 diz "porta aberta" e o passo 5 diz "BLOQUEADO". A mesma rede, dois resultados — é isso que kind: Dependency faz e uma rede sozinha não faz.

Goal: user network, internal DNS, and isolation — with zero DNS configuration.

# 1. Your own network
delonix network create shop

# 2. Database and app, both on it
delonix container run -d --name db  --net shop -e POSTGRES_PASSWORD=x postgres:16-alpine
delonix container run -d --name app --net shop -p 8080:80 nginx:alpine

# 3. The app resolves db BY NAME, no /etc/hosts, no env vars
delonix container exec app sh -c 'getent hosts db; nc -z db 5432 && echo "port open"'

# 4. Lock db down to everyone except app (DIRECTED reachability)
cat > dep.yaml <<'EOF'
apiVersion: delonix.io/v1
kind: Dependency
metadata:
  name: app-knows-db
spec:
  from: app
  to: db
  ports: ["5432"]
EOF
delonix stack apply -f dep.yaml

# 5. Prove it: a third container on the SAME network can no longer reach db
delonix container run --rm --net shop alpine sh -c 'nc -z -w2 db 5432 || echo BLOCKED'

delonix container rm -f db app; delonix network rm shop; rm dep.yaml

Verification: step 3 says "port open" and step 5 says "BLOCKED". Same network, two different outcomes — that's what kind: Dependency does that a plain network alone doesn't.

Do Dockerfile à imagem, sem daemonFrom Dockerfile to image, no daemon intermédiointermediate

Objectivo: construir uma imagem e correr o que construíste.

mkdir -p lab-build && cd lab-build
cat > Delonixfile <<'EOF'
FROM alpine:latest
RUN apk add --no-cache curl
COPY ola.txt /ola.txt
HEALTHCHECK CMD test -f /ola.txt
CMD ["sh", "-c", "cat /ola.txt; sleep 300"]
EOF
echo "construído com delonix" > ola.txt

# `Delonixfile` é procurado antes de `Dockerfile` — sem -f nenhum
delonix build -t minha-app:1.0 .

# `--wait` bloqueia até o HEALTHCHECK passar: sem isto escreve-se
# `until ...; do sleep 1; done`, e escreve-se mal
delonix container run -d --name a1 --wait --health-interval 2 minha-app:1.0
delonix container ps

delonix container rm -f a1; delonix image remove minha-app:1.0; cd ..; rm -rf lab-build

Verificação: o ps mostra (healthy) na coluna STATUS. O motor está a sondar o container sozinho — sem systemd, ao contrário do Podman rootless.

Goal: build an image and run what you built.

mkdir -p lab-build && cd lab-build
cat > Delonixfile <<'EOF'
FROM alpine:latest
RUN apk add --no-cache curl
COPY hello.txt /hello.txt
HEALTHCHECK CMD test -f /hello.txt
CMD ["sh", "-c", "cat /hello.txt; sleep 300"]
EOF
echo "built with delonix" > hello.txt

# `Delonixfile` is looked for before `Dockerfile` — no -f needed
delonix build -t my-app:1.0 .

# `--wait` blocks until HEALTHCHECK passes: without it you'd have to
# write `until ...; do sleep 1; done` yourself, and get it wrong
delonix container run -d --name a1 --wait --health-interval 2 my-app:1.0
delonix container ps

delonix container rm -f a1; delonix image remove my-app:1.0; cd ..; rm -rf lab-build

Verification: ps shows (healthy) in the STATUS column. The engine is probing the container on its own — no systemd, unlike rootless Podman.

Uma microVM a sérioA real microVM intermédiointermediate

Objectivo: uma VM completa com o mesmo CLI dos containers.

# 1. Sem imagem local, o create descarrega a oficial sozinho
delonix vm create dev

# 2. Onde ficou, e com que IP
delonix vm ls
delonix describe vms dev

# 3. Entrar (voltar ao host: Ctrl+])
delonix vm console dev

# 4. Checkpoint de sistema — memória E disco, com a VM a correr
delonix vm snapshot create dev limpa
delonix vm snapshot ls dev

# 5. Estragar alguma coisa lá dentro e voltar atrás
delonix vm snapshot restore dev limpa

delonix delete vms dev

Verificação: o passo 4 devolve sem erro com a VM A CORRER. Um snapshot de uma VM parada falha de propósito — o vm stop faz undefine do domínio para não deixar órfãos.

Goal: a full VM with the same CLI as containers.

# 1. With no local image, create downloads the official one on its own
delonix vm create dev

# 2. Where it landed, and with what IP
delonix vm ls
delonix describe vms dev

# 3. Go in (back to the host: Ctrl+])
delonix vm console dev

# 4. System checkpoint — memory AND disk, VM still running
delonix vm snapshot create dev clean
delonix vm snapshot ls dev

# 5. Break something inside and roll back
delonix vm snapshot restore dev clean

delonix delete vms dev

Verification: step 4 returns with no error and the VM STILL RUNNING. A snapshot of a stopped VM fails on purpose — vm stop undefines the domain to avoid leaving orphans.

A tua própria imagem de VM, com VMfileYour own VM image, with a VMfile avançadoadvanced

Objectivo: construir uma imagem qcow2 à tua medida, como se constrói uma imagem de container.

mkdir -p lab-vm && cd lab-vm

# Scaffold que CONSTRÓI COMO ESTÁ — apaga o que não precisares
delonix vm init --vmfile --name minha-base
cat VMfile

# Precisa de libguestfs no host: sudo apt install libguestfs-tools
delonix image vm build -t minha-base:1.0 .
delonix vm ls

# Um RUN com `apt-get install` precisa de rede no convidado, e pede-se:
#   delonix image vm build --network -t minha-base:1.0 .

# Arrancar a partir dela
delonix vm create teste --disk minha-base:1.0 --ssh-key @~/.ssh/id_ed25519.pub

# …ou a partir de um qcow2 publicado por ti, sem passar pelo store
delonix vm create outra --url-img https://o-teu-bucket/imagem.qcow2

delonix delete vms teste; cd ..; rm -rf lab-vm

Verificação: o build imprime [1/1] stage-1: FROM ubuntu:24.04 e verifica o checksum da cloud image. Com --url-img, se não houver <url>.sha256 publicado, o motor diz que está a confiar só no TLS — em vez de calar. A chave que injectas vai para a conta delonix, não para a conta default da distro — o bloco de próximos passos do vm create imprime o ssh exacto.

Goal: build your own qcow2 image, the same way you build a container image.

mkdir -p lab-vm && cd lab-vm

# Scaffold that BUILDS AS-IS — delete what you don't need
delonix vm init --vmfile --name my-base
cat VMfile

# Needs libguestfs on the host: sudo apt install libguestfs-tools
delonix image vm build -t my-base:1.0 .
delonix vm ls

# A RUN with `apt-get install` needs guest networking, so ask for it:
#   delonix image vm build --network -t my-base:1.0 .

# Boot from it
delonix vm create test --disk my-base:1.0 --ssh-key @~/.ssh/id_ed25519.pub

# …or from a qcow2 you published yourself, bypassing the store
delonix vm create other --url-img https://your-bucket/image.qcow2

delonix delete vms test; cd ..; rm -rf lab-vm

Verification: the build prints [1/1] stage-1: FROM ubuntu:24.04 and verifies the cloud image's checksum. With --url-img, if there's no published <url>.sha256, the engine says it's trusting TLS alone — instead of staying quiet. The key you inject goes to the delonix account, not the distro's default account — the "next steps" block from vm create prints the exact ssh command.

Kubernetes sem DockerKubernetes with no Docker avançadoadvanced

Objectivo: um cluster local cujo runtime dos nós É o Delonix.

# 1. Preflight — falha em milissegundos se faltar a delegação de `cpu`,
#    em vez de descarregar 425 MB para morrer aos 90 segundos
delonix system setup

# 2. O cluster
delonix cluster create --name lab

# 3. Falar com ele
export KUBECONFIG=~/.local/share/delonix/clusters/lab-kubeconfig.yaml
kubectl get nodes -o wide

# 4. Levar uma imagem TUA para dentro dos nós, sem registo nenhum
delonix build -t app:dev .
delonix cluster load app:dev --name lab
kubectl run app --image=app:dev --image-pull-policy=Never

# 5. A prova que interessa
kubectl get pod app -w

delonix delete clusters lab

Verificação: o passo 5 chega a Running. Nem o ctr images import nem o crictl images provam isto — os dois já reportaram sucesso sobre uma imagem que o kubelet depois não resolvia.

Goal: a local cluster whose node runtime IS Delonix.

# 1. Preflight — fails in milliseconds if `cpu` delegation is missing,
#    instead of downloading 425 MB just to die at the 90-second mark
delonix system setup

# 2. The cluster
delonix cluster create --name lab

# 3. Talk to it
export KUBECONFIG=~/.local/share/delonix/clusters/lab-kubeconfig.yaml
kubectl get nodes -o wide

# 4. Get an image of YOURS into the nodes, with no registry at all
delonix build -t app:dev .
delonix cluster load app:dev --name lab
kubectl run app --image=app:dev --image-pull-policy=Never

# 5. The proof that matters
kubectl get pod app -w

delonix delete clusters lab

Verification: step 5 reaches Running. Neither ctr images import nor crictl images prove this — both have reported success on an image the kubelet then couldn't resolve.

IaC de verdade: plan, apply convergente, e a recriação recusadaReal IaC: plan, converging apply, and a recreate that gets refused avançadoadvanced

Objectivo: o ciclo completo — plan → apply → plan outra vez (sem alterações) → mudar um campo QUENTE → mudar um campo FRIO — sem ficheiro de estado nenhum: o que já foi aplicado vive no PRÓPRIO recurso, não num .tfstate à parte.

mkdir -p lab-iac && cd lab-iac
cat > stack.yaml <<'EOF'
apiVersion: delonix.io/v1
kind: Network
metadata:
  name: iac-lab
spec: {}
---
apiVersion: delonix.io/v1
kind: Container
metadata:
  name: iac-app
spec:
  image: nginx:alpine
  network: iac-lab
  memory: 64M
EOF

# 1. Nada foi aplicado ainda — o plano di-lo, e o exit code também
delonix stack plan -f stack.yaml --detailed-exitcode; echo "exit=$?"   # 2 = há alterações

# 2. Aplicar
delonix stack apply -f stack.yaml

# 3. O MESMO plano, sobre o que acabou de ser criado
delonix stack plan -f stack.yaml --detailed-exitcode; echo "exit=$?"   # 0 = nada a fazer

# 4. Mudar um campo QUENTE — o plano propõe uma actualização, não uma recriação
sed -i 's/memory: 64M/memory: 128M/' stack.yaml
delonix stack plan -f stack.yaml     # memory: 64M → 128M
delonix stack apply -f stack.yaml    # "updating 1 field(s) live" — MESMO pid

# 5. Mudar um campo FRIO (a imagem) — o apply RECUSA-SE, não adivinha
sed -i 's/image: nginx:alpine/image: httpd:alpine/' stack.yaml
delonix stack apply -f stack.yaml    # refused: does not converge live: image

# 6. Só com a decisão explícita é que recria
delonix stack apply -f stack.yaml --replace Container/iac-app

delonix container rm -f iac-app; delonix network rm iac-lab; cd ..; rm -rf lab-iac

Verificação: no passo 4, o pid antes e depois (delonix container inspect iac-app | grep pid) é o MESMO — só o cgroup mudou. No passo 5 o apply sai com erro e nada muda — nem o container antigo é tocado — até correres o passo 6 de propósito. É a diferença entre uma ferramenta que converge e uma que só sabe criar: um terraform apply sobre um campo imutável também recria; a diferença aqui é que a recriação nunca é silenciosa, precisa do nome exacto do recurso, e sem ela o comando falha em vez de decidir por ti. Para remover o que o ficheiro deixou de declarar (sem tocar no resto), existe ainda stack apply --prune — não corrido aqui de propósito, para este lab nunca apagar mais do que os dois recursos que ele próprio criou.

Goal: the full cycle — plan → apply → plan again (nothing to do) → change a HOT field → change a COLD field — with no state file at all: what's already applied lives on the resource ITSELF, not in a .tfstate off to the side.

mkdir -p lab-iac && cd lab-iac
cat > stack.yaml <<'EOF'
apiVersion: delonix.io/v1
kind: Network
metadata:
  name: iac-lab
spec: {}
---
apiVersion: delonix.io/v1
kind: Container
metadata:
  name: iac-app
spec:
  image: nginx:alpine
  network: iac-lab
  memory: 64M
EOF

# 1. Nothing applied yet — the plan says so, and so does the exit code
delonix stack plan -f stack.yaml --detailed-exitcode; echo "exit=$?"   # 2 = changes pending

# 2. Apply it
delonix stack apply -f stack.yaml

# 3. The SAME plan, against what was just created
delonix stack plan -f stack.yaml --detailed-exitcode; echo "exit=$?"   # 0 = nothing to do

# 4. Change a HOT field — the plan proposes an update, not a recreate
sed -i 's/memory: 64M/memory: 128M/' stack.yaml
delonix stack plan -f stack.yaml     # memory: 64M → 128M
delonix stack apply -f stack.yaml    # "updating 1 field(s) live" — SAME pid

# 5. Change a COLD field (the image) — apply REFUSES, it doesn't guess
sed -i 's/image: nginx:alpine/image: httpd:alpine/' stack.yaml
delonix stack apply -f stack.yaml    # refused: does not converge live: image

# 6. Only an explicit decision recreates it
delonix stack apply -f stack.yaml --replace Container/iac-app

delonix container rm -f iac-app; delonix network rm iac-lab; cd ..; rm -rf lab-iac

Verification: in step 4, the pid before and after (delonix container inspect iac-app | grep pid) is the SAME — only the cgroup changed. In step 5 apply exits with an error and nothing changes — not even the old container is touched — until you deliberately run step 6. That's the difference between a tool that converges and one that only knows how to create: a terraform apply over an immutable field also recreates; the difference here is that the recreate is never silent, it needs the exact resource name, and without it the command fails instead of deciding for you. To remove what the file no longer declares (without touching anything else), there's also stack apply --prune — not run here on purpose, so this lab never deletes more than the two resources it created itself.