Declare a Stack and Check It Against Reality
This lab runs on a real VM
This box is not a Pod but a virtual machine launched by KubeVirt. A separate Linux kernel runs in it, systemd actually manages services, and docker is a real Docker engine, not an imitation. A container started with docker run becomes a real process, and both docker exec and docker logs work as usual.
This lab used to run inside a Pod. That box had dropped every kernel capability, so the step that starts a container was blocked, and you learned by working around it and unpacking image archives by hand. The workaround is no longer needed.
There are two things to know.
- The first start takes a little over a minute. The VM boots and installs Docker, so it is slower than a Pod lab (usually 40 seconds).
- There is no browser preview. Only one grading port is open for connections into the VM. If you start a web server, check it with
curlfrom inside the VM.
Goal
You declare a stack of two services in a Compose file, start the same stack by hand as well, and compare whether the declared values and the actual values match exactly.
Why it matters
If you understand Compose only as "a tool that starts several containers at once", you get only half of it. The real value is that the shape of the stack is written in a file and remains in the repository. And this value holds only when the file and reality match. The moment someone fixes a container by hand, the file is no longer documentation but a lie, and nobody knows about that lie until the next deployment. That is why the last step of this lab is not building a new feature but comparison. The habit that every team using declarative tools must have is here.
Steps
Write /root/ops3/compose.yaml with the structure below. Indent with 2 spaces, and the top-level keys (services:, volumes:, networks:) must start at the very beginning of the line.
- Put
services:at the top level and, under it, declare the two servicesweb:andcache:with 2-space indentation. - Set
web'simagetonginx:1.27-alpineandcache'simagetoalpine:3.20, and forcachealso write acommandso that it keeps running. - Put
networks:at the top level and declareappnet:under it, then, in thenetworks:list of both services, put- appnet. - In
web, addports:and put"127.0.0.1:8081:80"in it. Oncache, do not putports. - Put
volumes:at the top level and declarewebdata:, then, inweb'svolumes:, putwebdata:/usr/share/nginx/html. - In
cache, useenvironment:to putCACHE_TTLwith the value60, and inweb, usedepends_on:to put- cache. - Start the same stack by hand. The network name is
ops3net, and the container names areops3-web(nginx:1.27-alpine, publishing127.0.0.1:8081:80) andops3-cache(alpine:3.20, environment variableCACHE_TTL=60), and you attach both toops3net. - There is nothing new to create in this step. You confirm that the declaration up to step 6 and the actual containers of step 7 match exactly on the three items: image, port and environment variable.
Notes
- Putting the top-level keys in the order
services:→volumes:→networks:makes it easy to read. - In this lab, the step 8 grading directly compares the values obtained by parsing the compose file with the
docker inspectvalues. - Common mistake 1: if you indent the service names by 4 spaces, the structure changes. It is exactly 2 spaces.
- Common mistake 2: if you write the image tag differently in step 7, such as
nginx:alpine, it will not match in the step 8 comparison.
Declare two services
Put services: at the top level and, under it, declare the two services web: and cache: with 2-space indentation.
Put the service names under the top-level services with 2-space indentation. If the indentation is off, the meaning of the whole file changes.
Specify the image and command
Set web's image to nginx:1.27-alpine and cache's image to alpine:3.20, and for cache also write a command so that it keeps running.
alpine's default command ends immediately, so you must also write a command that keeps running.
Declare a shared network
Put networks: at the top level and declare appnet: under it, then, in the networks: list of both services, put - appnet.
Declare networks at the top level and refer to that name from each service. Both places are needed.
Ports only on the loopback
In web, add ports: and put "127.0.0.1:8081:80" in it. On cache, do not put ports.
You can prefix the port string with a binding address. Do not use ports for a service that only does internal communication.
Declare a named volume
Put volumes: at the top level and declare webdata:, then, in web's volumes:, put webdata:/usr/share/nginx/html.
Declare the name in the top-level volumes and use it in the service together with the target path.
Environment variable and startup order
In cache, use environment: to put CACHE_TTL with the value 60, and in web, use depends_on: to put - cache.
Remember that depends_on guarantees only order, not readiness.
Start the same stack by hand
Start the same stack by hand. The network name is ops3net, and the container names are ops3-web (nginx:1.27-alpine, publishing 127.0.0.1:8081:80) and ops3-cache (alpine:3.20, environment variable CACHE_TTL=60), and you attach both to ops3net.
Create the network first and then attach the containers. Use exactly the same values as those written in the declaration file.
Compare the declaration with reality
There is nothing new to create in this step. You confirm that the declaration up to step 6 and the actual containers of step 7 match exactly on the three items: image, port and environment variable.
Grading directly compares the values read from the compose file with the inspect values of the actual containers. If even one character differs, they are out of sync.