GuíasGuides · Nivel 3Level 3

Docker: montar y mantener contenedores en un servidor de operacionesDocker: running and maintaining containers on an operations server

Instalar Docker, arrancar una herramienta interna con Compose, dejar los datos en volúmenes y saber qué mirar cuando un contenedor se reinicia solo.Install Docker, start an internal tool with Compose, keep the data in volumes and know what to look at when a container keeps restarting.

Qué vas a conseguir

Un servidor Linux con Docker donde las herramientas internas (un panel, un Grafana, un runner de Jenkins) corren como contenedores: se arrancan con un fichero, se actualizan cambiando una versión y sus datos sobreviven a los reinicios. Y la rutina de diagnóstico cuando uno falla.

Antes de empezar

  • Un servidor Ubuntu o RHEL ya configurado (guías del Nivel 2), con 20 GB libres en /var/lib/docker.
  • Saber qué puertos va a exponer cada herramienta: el firewall del servidor tiene que dejarlos pasar.
  • Un lugar donde guardar los ficheros Compose bajo Git.

1. Instalar y comprobar

bash · srv-tools-01
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh...sudo usermod -aG docker ansible && newgrp dockerdocker version --format '{{.Server.Version}}'27.3.1docker run --rm hello-world | head -3Hello from Docker!This message shows that your installation appears to be working correctly.docker system dfTYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLEImages          1         0         13.3kB    13.3kB (100%)Containers      0         0         0B        0BLocal Volumes   0         0         0B        0B

El usuario en el grupo docker equivale a root en ese servidor: solo quien administra la máquina debe estar en él.

2. La primera herramienta con Compose

docker-compose.yml
services:  grafana:    image: grafana/grafana-oss:11.3.0    container_name: grafana    restart: unless-stopped    ports:      - "3000:3000"    environment:      GF_SERVER_ROOT_URL: https://tools.contoso.local/grafana/      GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin    volumes:      - grafana-data:/var/lib/grafana    secrets:      - grafana_admin    healthcheck:      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"]      interval: 30s      timeout: 5s      retries: 3 volumes:  grafana-data: secrets:  grafana_admin:    file: ./secrets/grafana_admin.txt

Tres decisiones que te ahorran problemas: la versión fija de la imagen (nunca latest en un servidor), los datos en un volumen con nombre (no dentro del contenedor) y la contraseña en un secreto desde fichero, que va al .gitignore.

bash · srv-tools-01
mkdir -p ~/stacks/grafana/secrets && cd ~/stacks/grafanaopenssl rand -base64 24 > secrets/grafana_admin.txt && chmod 600 secrets/grafana_admin.txtecho "secrets/" >> .gitignoredocker compose up -d[+] Running 2/2 ✔ Volume "grafana_grafana-data"  Created ✔ Container grafana             Starteddocker compose psNAME      IMAGE                       STATUS                   PORTSgrafana   grafana/grafana-oss:11.3.0  Up 40 seconds (healthy)  0.0.0.0:3000->3000/tcpsudo firewall-cmd --add-port=3000/tcp --permanent && sudo firewall-cmd --reload

3. Cuando un contenedor se cae

bash · srv-tools-01
docker compose psNAME      IMAGE                       STATUSgrafana   grafana/grafana-oss:11.3.0  Restarting (1) 12 seconds agodocker compose logs --tail 30 grafanagrafana  | Error: ✗ failed to connect to database: open /var/lib/grafana/grafana.db: permission denieddocker inspect grafana --format '{{.State.ExitCode}} {{.State.Error}} {{.RestartCount}}'1  7docker run --rm -v grafana_grafana-data:/data alpine ls -ln /data | head -3total 1032-rw-r----- 1 0 0 1056768 Oct 22 07:40 grafana.dbdocker run --rm -v grafana_grafana-data:/data alpine chown -R 472:0 /datadocker compose restart grafana && sleep 20 && docker compose ps --format '{{.Name}} {{.Status}}'grafana Up 20 seconds (healthy)

El orden siempre es el mismo: ps (qué estado), logs --tail (qué dice), inspect (código de salida y reinicios). Un contenedor en Restarting con el mismo error en bucle no se arregla reiniciando: se arregla leyendo el log. Aquí el volumen había sido restaurado con root y Grafana corre como el usuario 472.

Si el problema es de red, docker exec -it grafana sh te mete dentro para hacer wget a lo que el contenedor no alcanza, y docker network inspect bridge enseña qué IP tiene cada uno.

4. Actualizar sin perder datos

bash · srv-tools-01
docker run --rm -v grafana_grafana-data:/data -v $PWD/backup:/backup alpine tar czf /backup/grafana-data-2026-10-22.tgz -C /data .sed -i 's/grafana-oss:11.3.0/grafana-oss:11.3.1/' docker-compose.ymldocker compose pull && docker compose up -d[+] Running 1/1 ✔ Container grafana  Starteddocker compose logs --tail 3 grafana | grep -i "http server listen"grafana  | logger=http.server t=2026-10-22T08:01:12Z level=info msg="HTTP Server Listen" address=[::]:3000

Copia del volumen, cambio de versión en el fichero, pull, up -d. Si la nueva versión falla, vuelves a la anterior con el mismo sed y, si hace falta, restauras el .tgz. El fichero Compose con el cambio de versión es el commit que va al ticket.

5. Limpieza mensual

bash · srv-tools-01
docker system dfTYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLEImages          9         3         4.1GB     2.7GB (65%)Build Cache     41        0         1.2GB     1.2GBdocker image prune -a --filter "until=720h"docker builder prune -fdocker system dfTYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLEImages          3         3         1.4GB     0B (0%)

Nunca ejecutes docker volume prune en un servidor sin revisar antes docker volume ls: borra los volúmenes que ningún contenedor usa en ese momento, incluidos los de un servicio parado a propósito.

Siguientes pasos

  • Pon un proxy inverso (Traefik o Caddy) delante de los contenedores para que todos salgan por 443 con certificado y sin abrir un puerto por herramienta.
  • Mete docker compose pull && up -d en el playbook de Ansible: la actualización de las herramientas pasa a ser una tarea más del parcheo.
  • Con más de un servidor o con alta disponibilidad, el salto es Kubernetes (Nivel 4); hasta entonces, Compose en un servidor bien respaldado es suficiente para las herramientas internas.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

What you'll get

A Linux server with Docker where internal tools (a dashboard, a Grafana, a Jenkins runner) run as containers: they start from one file, they update by changing a version, and their data survives restarts. Plus the diagnosis routine when one fails.

Before you start

  • An Ubuntu or RHEL server already configured (Level 2 guides), with 20 GB free in /var/lib/docker.
  • Know which ports each tool will expose: the server firewall has to let them through.
  • A place to keep the Compose files under Git.

1. Install and check

bash · srv-tools-01
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh...sudo usermod -aG docker ansible && newgrp dockerdocker version --format '{{.Server.Version}}'27.3.1docker run --rm hello-world | head -3Hello from Docker!This message shows that your installation appears to be working correctly.docker system dfTYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLEImages          1         0         13.3kB    13.3kB (100%)Containers      0         0         0B        0BLocal Volumes   0         0         0B        0B

A user in the docker group is effectively root on that server: only the people who administer the machine belong in it.

2. The first tool with Compose

docker-compose.yml
services:  grafana:    image: grafana/grafana-oss:11.3.0    container_name: grafana    restart: unless-stopped    ports:      - "3000:3000"    environment:      GF_SERVER_ROOT_URL: https://tools.contoso.local/grafana/      GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin    volumes:      - grafana-data:/var/lib/grafana    secrets:      - grafana_admin    healthcheck:      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"]      interval: 30s      timeout: 5s      retries: 3 volumes:  grafana-data: secrets:  grafana_admin:    file: ./secrets/grafana_admin.txt

Three decisions that save you trouble: a pinned image version (never latest on a server), data in a named volume (not inside the container) and the password as a secret from a file, which goes in .gitignore.

bash · srv-tools-01
mkdir -p ~/stacks/grafana/secrets && cd ~/stacks/grafanaopenssl rand -base64 24 > secrets/grafana_admin.txt && chmod 600 secrets/grafana_admin.txtecho "secrets/" >> .gitignoredocker compose up -d[+] Running 2/2 ✔ Volume "grafana_grafana-data"  Created ✔ Container grafana             Starteddocker compose psNAME      IMAGE                       STATUS                   PORTSgrafana   grafana/grafana-oss:11.3.0  Up 40 seconds (healthy)  0.0.0.0:3000->3000/tcpsudo firewall-cmd --add-port=3000/tcp --permanent && sudo firewall-cmd --reload

3. When a container dies

bash · srv-tools-01
docker compose psNAME      IMAGE                       STATUSgrafana   grafana/grafana-oss:11.3.0  Restarting (1) 12 seconds agodocker compose logs --tail 30 grafanagrafana  | Error: ✗ failed to connect to database: open /var/lib/grafana/grafana.db: permission denieddocker inspect grafana --format '{{.State.ExitCode}} {{.State.Error}} {{.RestartCount}}'1  7docker run --rm -v grafana_grafana-data:/data alpine ls -ln /data | head -3total 1032-rw-r----- 1 0 0 1056768 Oct 22 07:40 grafana.dbdocker run --rm -v grafana_grafana-data:/data alpine chown -R 472:0 /datadocker compose restart grafana && sleep 20 && docker compose ps --format '{{.Name}} {{.Status}}'grafana Up 20 seconds (healthy)

The order is always the same: ps (what state), logs --tail (what it says), inspect (exit code and restart count). A container in Restarting with the same error in a loop is not fixed by restarting: it is fixed by reading the log. Here the volume had been restored as root and Grafana runs as user 472.

If the problem is network, docker exec -it grafana sh gets you inside to wget whatever the container can't reach, and docker network inspect bridge shows which IP each one has.

4. Update without losing data

bash · srv-tools-01
docker run --rm -v grafana_grafana-data:/data -v $PWD/backup:/backup alpine tar czf /backup/grafana-data-2026-10-22.tgz -C /data .sed -i 's/grafana-oss:11.3.0/grafana-oss:11.3.1/' docker-compose.ymldocker compose pull && docker compose up -d[+] Running 1/1 ✔ Container grafana  Starteddocker compose logs --tail 3 grafana | grep -i "http server listen"grafana  | logger=http.server t=2026-10-22T08:01:12Z level=info msg="HTTP Server Listen" address=[::]:3000

Volume backup, version change in the file, pull, up -d. If the new version fails, you go back to the previous one with the same sed and, if needed, restore the .tgz. The Compose file with the version change is the commit that goes in the ticket.

5. Monthly clean-up

bash · srv-tools-01
docker system dfTYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLEImages          9         3         4.1GB     2.7GB (65%)Build Cache     41        0         1.2GB     1.2GBdocker image prune -a --filter "until=720h"docker builder prune -fdocker system dfTYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLEImages          3         3         1.4GB     0B (0%)

Never run docker volume prune on a server without checking docker volume ls first: it deletes the volumes no container is using at that moment, including those of a service you stopped on purpose.

Next steps

  • Put a reverse proxy (Traefik or Caddy) in front of the containers so everything comes out on 443 with a certificate, without opening one port per tool.
  • Add docker compose pull && up -d to the Ansible playbook: updating the tools becomes one more patching task.
  • With more than one server or with high availability, the jump is Kubernetes (Level 4); until then, Compose on a well backed-up server is enough for internal tools.

Is something missing, or different in your environment? Get in touch and I'll fix it.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →