Docker Compose è uno strumento che ti permette di far partire più container insieme come se fossero un’unica applicazione.
Invece di lanciare ogni container manualmente con numerosi comandi separati è possibile descrivere l’intero ambiente all’interno di un unico file di configurazione chiamato compose.yml.
In questo file vengono definiti tutti i componenti necessari: quali servizi devono partire, quali immagini utilizzare, quali porte esporre, quali volumi montare e in che modo i container devono comunicare tra loro.
Una volta scritta la configurazione è sufficiente un solo comando docker compose up oppure podman-compose up per avviare automaticamente l’intero stack già funzionante e correttamente collegato.
Il principale vantaggio di Compose è la semplicità con cui consente di lavorare con applicazioni composte da più servizi, ad esempio un web server, un backend applicativo e un database. Tutta la configurazione rimane concentrata in un unico file leggibile, versionabile e facilmente condivisibile, permettendo a chiunque di ricreare lo stesso ambiente in pochi secondi.
Questo approccio è ideale per lo sviluppo locale e per i laboratori di test, perché consente di avviare, fermare e ricreare l’intero progetto in modo rapido e ripetibile, utilizzando sempre la stessa configurazione sia con Docker che con Podman.
Simulazione di una cache con PostgreSQL e Redis
Simuliamo una cache manualmente usando PostgreSQL come database principale e Redis come cache veloce. PostgreSQL è il database che conserva i dati in modo affidabile su disco Redis invece viene usato come cache in memoria veloce e temporanea per alleggerire le letture.
Prima proviamo a leggere un dato da Redis (cache) e se non c’è lo recuperiamo da PostgreSQL e lo salviamo in Redis con un TTL, infine, quando aggiorniamo il dato su PostgreSQL invalidiamo la cache per evitare valori vecchi.
STEP 1. Creazione del file docker-compose.yml ed avvio dei container
frank@debian:~$ nano compose.yml
name: labcache
services:
postgres:
image: postgres:15
container_name: pg01
environment:
POSTGRES_PASSWORD: test123
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
networks:
- backend_net
redis:
image: redis:7
container_name: redis01
ports:
- "6379:6379"
networks:
- backend_net
volumes:
pg_data:
name: pg_data_custom
networks:
backend_net:
name: backend_network_custom
frank@debian:~$ docker compose up -d
[+] up 32/32
✔ Image postgres:15 Pulled 94.2s
✔ Image redis:7 Pulled 45.4s
✔ Network backend_network_custom Created 0.2s
✔ Volume pg_data_custom Created 0.0s
✔ Container pg01 Created 4.3s
✔ Container redis01 Created 4.3s
frank@debian:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a181ff53d06c postgres:15 "docker-entrypoint.s…" 47 seconds ago Up 43 seconds 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp pg01
57ed3f2b04bd redis:7 "docker-entrypoint.s…" 47 seconds ago Up 43 seconds 0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp redis01
frank@debian:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
1788a93e0136 backend_network_custom bridge local
117eb0fc129d bridge bridge local
a8820964de30 host host local
597294692ce2 none null local
frank@debian:~$ docker volume ls
DRIVER VOLUME NAME
local 3e3a65363d30f36f1aaea5629d59a030c2cb61754859047cd87da025b41bc268
local pg_data_custom
Container e persistenza dei volumi varie opzioni:
$ docker compose up -d # avvia senza toccare i dati
$ docker compose down # rimuove container ma lascia i dati
$ docker compose down -v # rimuove container e volumi (dati persi)
$ docker rm -f pg01 # elimina container singolo (dati persi se senza volume)
frank@debian:~$ docker volume inspect pg_data_custom
[
{
"CreatedAt": "2026-02-15T15:59:01+01:00",
"Driver": "local",
"Labels": {
"com.docker.compose.config-hash": "6f734250da016b332d779ab2e3c27a15a4320013dd1f99337a66b8a9a6b432e8",
"com.docker.compose.project": "labcache",
"com.docker.compose.version": "5.0.2",
"com.docker.compose.volume": "pg_data"
},
"Mountpoint": "/var/lib/docker/volumes/pg_data_custom/_data",
"Name": "pg_data_custom",
"Options": null,
"Scope": "local"
}
]
frank@debian:~$ docker network inspect backend_network_custom
[
{
"Name": "backend_network_custom",
"Id": "1788a93e0136fcda442a2465b6227a7221016e5499c3c4bec376abd8674ee08b",
"Created": "2026-02-15T15:59:01.614981239+01:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv4": true,
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Options": {},
"Labels": {
"com.docker.compose.config-hash": "1c9b6b634dd19c526b592e11aebca8aa53b756d10a21750d259b92025705d61b",
"com.docker.compose.network": "backend_net",
"com.docker.compose.project": "labcache",
"com.docker.compose.version": "5.0.2"
},
"Containers": {
"57ed3f2b04bd1d6359e63a09da285ad242b66a8f2f5b961c9ab298e3352641e3": {
"Name": "redis01",
"EndpointID": "a89c39d80cf7aba5c4a162db7b13ad1aec4d6738be1e8bd925415fc7ce4187d8",
"MacAddress": "fa:58:43:16:7d:cc",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
},
"a181ff53d06c52794607ec6849a9e3756adbb5a3b1c03278b04c660b46b1c772": {
"Name": "pg01",
"EndpointID": "802d069573e43a5bfe0e5c29233a48bef3f35a92f9156cb98055f92232e9e852",
"MacAddress": "92:93:51:0a:c8:d2",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
}
},
"Status": {
"IPAM": {
"Subnets": {
"172.18.0.0/16": {
"IPsInUse": 5,
"DynamicIPsAvailable": 65531
}
}
}
}
}
]
STEP 2. La scrittura dei dati avviene prima su PostgreSQL
Inseriamo un dato in PostgreSQL entrando nel database:frank@debian:~$ docker exec -it pg01 psql -U postgres
psql (15.16 (Debian 15.16-1.pgdg13+1))
Type "help" for help.
postgres=# CREATE TABLE IF NOT EXISTS utenti (id SERIAL PRIMARY KEY, nome TEXT);
postgres=# INSERT INTO utenti (nome) VALUES ('Frank');
postgres=# SELECT * FROM utenti;
postgres=# \q
STEP 3. Simuliamo una lettura con cache
Entro nella console interattiva di Redis:frank@debian:~$ docker exec -it redis01 redis-cli
Legge il valore salvato dentro Redis usando la chiave utente:1
127.0.0.1:6379> GET utente:1
(nil)
127.0.0.1:6379> QUIT
Vuol dire che in Redis la chiave utente:1 non esiste o è scaduta se avevi messo un TTL. Se la chiave non esiste recuperiamo il dato dal database:
frank@debian:~$ docker exec -it pg01 psql -U postgres -t -c "SELECT nome FROM utenti WHERE id=1;"
Frank
STEP 4. Salvataggio in Redis con TTL
Entro nella console interattiva di Redis e salvo il risultato in Redis con TTL di 60 secondi:frank@debian:~$ docker exec -it redis01 redis-cli
127.0.0.1:6379> SETEX utente:1 60 "Frank"
OK
Verifichiamo che ora la chiave esista:
127.0.0.1:6379> GET utente:1
"Frank"
Controlliamo il TTL (secondi rimanenti) restituisce il Time To Live in secondi:
127.0.0.1:6379> TTL utente:1
(integer) 42
(integer) 42 → la chiave scadrà tra 42 secondi
(integer) -1 → la chiave esiste ma NON ha scadenza
(integer) -2 → la chiave non esiste
Usciamo da Redis:
127.0.0.1:6379> QUIT
Ora il dato è in cache: le prossime letture verranno soddisfatte direttamente da Redis finché il TTL non scade.
STEP 5. Aggiornamento su PostgreSQL e invalidazione della cache
Se modifico il nome utente la scrittura deve avvenire prima su PostgreSQL:frank@debian:~$ docker exec -it pg01 psql -U postgres -c "UPDATE utenti SET nome='Francesco' WHERE id=1;"
UPDATE 1
Dopo l’aggiornamento bisogna invalidare la cache:
frank@debian:~$ docker exec -it redis01 redis-cli
127.0.0.1:6379> DEL utente:1
(integer) 1
(integer) 1 = la chiave esisteva ed è stata eliminata
(integer) 0 = la chiave non esisteva
127.0.0.1:6379> QUIT
Alla prossima lettura la cache verrà ricreata recuperando il valore aggiornato da PostgreSQL.
STEP 6. Salvataggio del nuovo dato in Redis con TTL
Entro nella console interattiva di Redis e salvo il risultato in Redis con TTL di 60 secondi:frank@debian:~$ docker exec -it redis01 redis-cli
127.0.0.1:6379> SETEX utente:1 60 "Francesco"
OK
Verifichiamo che ora la chiave esista:
127.0.0.1:6379> GET utente:1
"Francesco"
Controlliamo il TTL (secondi rimanenti). Controlliamo il TTL (secondi rimanenti) restituisce il Time To Live in secondi:
127.0.0.1:6379> TTL utente:1
(integer) 40
(integer) 40 → la chiave scadrà tra 40 secondi
(integer) -1 → la chiave esiste ma NON ha scadenza
(integer) -2 → la chiave non esiste
Usciamo da Redis:
127.0.0.1:6379> QUIT
Ora il dato è in cache e le prossime letture verranno soddisfatte direttamente da Redis finché il TTL non scade.


