Probado con PostgreSQL 18.6 · postgres:18.6-alpine / 18.6-trixie · Docker Engine 29.5 · Compose 2.40 · verificado

Actualizado: 2026-09-16

PostgreSQL es la base de datos relacional que recomiendan o exigen aplicaciones self-hosted como Wiki.js u Outline, y con Docker arranca en pocos segundos una vez descargada la imagen. En esta guía levantas un servidor PostgreSQL 18.6 con un solo archivo docker-compose.yml, guardas los datos en un volumen persistente y ves qué hace la imagen 18 si montas la ruta de datos antigua. Después configuras las variables de entorno imprescindibles, conectas otros contenedores por red interna, haces copias de seguridad con pg_dump y añades un healthcheck para que la base de datos arranque de forma fiable. La misma explicación está disponible en inglés.

Puntos clave

  • PostgreSQL se presenta como la base de datos relacional de código abierto más avanzada del mundo. Su versión mayor estable más reciente es PostgreSQL 18, publicada el 25 de septiembre de 2025, y la menor actual es la 18.6, del 13 de agosto de 2026.
  • La imagen oficial postgres en Docker Hub tiene variantes Debian (trixie por defecto, también bookworm) y Alpine (postgres:18.6-alpine, hoy sobre Alpine 3.24). Según la API de Docker Hub, consultada el 16 de septiembre de 2026, la descarga comprimida para amd64 ocupa 120,0 MB en Alpine frente a 162,4 MB en Debian trixie.
  • Fija la versión menor, como postgres:18.6-alpine, en vez de :latest. Una etiqueta flotante puede saltar de versión mayor en el siguiente docker compose pull, y 18-alpine cambia de versión menor sin que lo decidas.
  • En PostgreSQL 18 la ruta de datos cambió: el volumen se monta en /var/lib/postgresql (antes era /var/lib/postgresql/data) y el clúster vive en /var/lib/postgresql/18/docker. Con la ruta antigua, el contenedor de la 18 se niega a arrancar.
  • El servidor escucha en el puerto 5432 y los demás contenedores de la misma pila se conectan a él por el nombre del servicio (db), sin necesidad de publicar el puerto en Internet.

¿Por qué ejecutar PostgreSQL en Docker?

PostgreSQL es un sistema de gestión de bases de datos objeto-relacional con más de 35 años de desarrollo a sus espaldas. Nextcloud, Paperless-ngx, Wiki.js, Gitea y Outline pueden usarla como almacén principal, y Outline la exige. Ejecutarla en un contenedor tiene una ventaja clara: en lugar de instalar paquetes en el sistema anfitrión, gestionar repositorios y arrastrar dependencias, arrancas una imagen ya preparada y aislada. Toda la configuración queda en un archivo que puedes versionar.

La imagen oficial resuelve el trabajo pesado por ti. La primera vez que arranca, ejecuta initdb, crea el usuario superadministrador y la base de datos inicial a partir de las variables de entorno, y deja el servidor escuchando en el puerto 5432. Si algún día quieres almacenar embeddings y hacer búsqueda semántica, existe una variante con la extensión pgvector, que tratamos en la guía de PostgreSQL con pgvector. Aquí nos centramos en PostgreSQL puro como base de datos relacional de propósito general.

¿Cómo es el docker-compose.yml con volumen persistente?

Crea una carpeta para el proyecto y guarda dentro este docker-compose.yml. Cambia el valor cambia_esta_clave_segura por una contraseña propia y robusta antes de arrancar:

services:
  db:
    image: postgres:18.6-alpine
    restart: unless-stopped
    shm_size: 128mb
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: cambia_esta_clave_segura
      POSTGRES_DB: appdb
    volumes:
      - pg_data:/var/lib/postgresql
    ports:
      - "127.0.0.1:5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pg_data:

Merece la pena detenerse en tres detalles. El volumen con nombre pg_data se monta en /var/lib/postgresql, la ruta correcta a partir de PostgreSQL 18, y la imagen guarda el clúster en su subdirectorio 18/docker. Si copias una guía antigua que monta /var/lib/postgresql/data, el contenedor no llega a arrancar, como verás en la sección siguiente. Para entender la diferencia entre un volumen con nombre y un bind mount, revisa la guía sobre volúmenes y bind mounts en Docker.

La opción shm_size: 128mb amplía la memoria compartida, porque Docker limita /dev/shm a 64 MB por defecto (lo comprobé con df dentro del contenedor) y las consultas paralelas o los índices grandes pueden quedarse cortos. Y al publicar el puerto como 127.0.0.1:5432:5432 lo dejas accesible solo desde la propia máquina, nunca expuesto a Internet.

Con el archivo listo, arranca la pila en segundo plano y comprueba el estado:

docker compose up -d
docker compose ps
docker compose logs -f db

La primera vez, PostgreSQL inicializa el directorio de datos y crea la base appdb. En mis pruebas con la 18.6 (arm64, 18 núcleos, carga media de entre 1 y 24 por otros procesos), el registro mostró database system is ready to accept connections entre 2 y 4 s después de docker compose up -d. A partir de esa línea, el servidor está operativo.

¿Qué pasa si montas /var/lib/postgresql/data con PostgreSQL 18?

El contenedor se niega a arrancar, y como no llega a escribir nada, tampoco pierde nada. Lo probé con postgres:18.6-alpine y postgres:18.6-trixie, con el volumen vacío y cambiando solo su línea a pg_data:/var/lib/postgresql/data. En las dos variantes el proceso sale con código 1, restart: unless-stopped lo relanza en bucle (Restarting (1)) y docker compose logs --no-log-prefix db empieza así:

Error: in 18+, these Docker images are configured to store database data in a
       format which is compatible with "pg_ctlcluster" (specifically, using
       major-version-specific directory names).  This better reflects how
       PostgreSQL itself works, and how upgrades are to be performed.

       See also https://github.com/docker-library/postgres/pull/1259

       Counter to that, there appears to be PostgreSQL data in:
         /var/lib/postgresql/data (unused mount/volume)

El mensaje sale del script de entrada de la imagen, docker-entrypoint.sh, que es idéntico en las variantes Alpine y Debian de la 18.6. Cuando PGDATA tiene su valor por defecto y no contiene un clúster, el script busca un archivo PG_VERSION en /var/lib/postgresql, /var/lib/postgresql/data y /var/lib/postgresql/*/docker. Si no hay datos pero /var/lib/postgresql/data es un punto de montaje, lo anota como unused mount/volume y ejecuta exit 1 antes de initdb.

Mientras tanto, Docker crea un volumen anónimo para /var/lib/postgresql, la ruta que la imagen declara como VOLUME, y dentro solo queda el directorio vacío 18/docker. Tu volumen pg_data sigue vacío. Tras docker compose down y up el error se repite, el volumen anónimo anterior queda huérfano y aparece otro. Si arrancas con docker compose up -d --wait, Compose termina con código 1 y marca el contenedor como unhealthy en vez de dejar el bucle en silencio.

Con un volumen que ya contiene un clúster de la 17 ocurre lo mismo. Lo creé con postgres:17.11 y con postgres:17.11-alpine, y al pasar a la 18.6 el mensaje señala /var/lib/postgresql/data, o /var/lib/postgresql si mueves el volumen a la ruta nueva. La 18 no modifica esos archivos: al volver a la 17.11, la fila de prueba seguía allí.

Tampoco cambies de variante sobre un volumen existente, porque el usuario postgres tiene el UID 999 en Debian y el 70 en Alpine. Un volumen creado con postgres:17.11 y abierto con postgres:18.6-alpine falló con mkdir: can't create directory '/var/lib/postgresql/18/': Permission denied en la ruta nueva, y con el aviso unused mount/volume en la antigua.

Para pasar los datos de la 17 a la 18, la vía que probé es la copia lógica de la sección de copias de seguridad. Con la 17 aún en marcha, vuelca la base con pg_dump -Fc, levanta la 18 con un volumen nuevo en /var/lib/postgresql y restaura con pg_restore. La alternativa es pg_upgrade --link, que la nueva estructura de directorios facilita según el propio mensaje de error; no la he probado para esta guía.

El error contrario sí pierde datos. La imagen postgres:17 sigue declarando su VOLUME en /var/lib/postgresql/data, así que con este docker-compose.yml escribe en un volumen anónimo. Lo comprobé con la 17.11: tras down y up, el contenedor inicializó una base vacía y la consulta respondió relation "notas" does not exist.

La comprobación de la 18 tiene una excepción, porque solo se aplica con el PGDATA por defecto. Una guía antigua que además fija PGDATA: /var/lib/postgresql/data/pgdata arranca con la 18.6 y conserva los datos tras down y up, aunque cada recreación deja otro volumen anónimo sin uso.

¿Qué variables de entorno necesita la imagen?

La imagen se configura por completo con variables de entorno en el primer arranque. Solo una es obligatoria:

  • POSTGRES_PASSWORD: la contraseña del superusuario. No tiene valor por defecto, así que sin ella el contenedor se niega a arrancar.
  • POSTGRES_USER: el nombre del superusuario. Si lo omites, se usa postgres.
  • POSTGRES_DB: la base de datos que se crea al inicio. Si no la indicas, toma el mismo nombre que POSTGRES_USER.
  • POSTGRES_INITDB_ARGS: argumentos extra para initdb, por ejemplo --no-data-checksums. La 18 ya activa por defecto las sumas de verificación que detectan corrupción en disco (SHOW data_checksums devolvió on), y pg_upgrade exige que el clúster antiguo y el nuevo coincidan en ese ajuste.

Un detalle importante: estas variables solo surten efecto la primera vez, cuando el volumen está vacío y se ejecuta initdb. Si cambias POSTGRES_PASSWORD con datos ya existentes, la contraseña no se actualiza sola: en mi prueba, la clave nueva falló con password authentication failed for user "app" y tendrás que cambiarla desde dentro con ALTER USER. Por eso conviene no meter contraseñas en texto plano dentro del docker-compose.yml de producción: lo recomendable es cargarlas desde un archivo .env o, mejor aún, con variables de entorno y secretos en Docker.

¿Cómo conectarse a PostgreSQL desde otros contenedores?

Aquí es donde Docker brilla. Todos los servicios definidos en el mismo docker-compose.yml comparten una red interna y se descubren por su nombre de servicio. Una aplicación en la misma pila se conecta a la base de datos usando db como host y 5432 como puerto, sin necesidad de publicar nada al exterior. La cadena de conexión típica sería:

postgres://app:cambia_esta_clave_segura@db:5432/appdb

Fíjate en que el host es db, no localhost ni una dirección IP: dentro de la red de Compose, el DNS interno de Docker resuelve db a la IP del contenedor de la base de datos. Para una app que dependa de PostgreSQL, lo ideal es que no arranque hasta que la base de datos esté sana. Combina el healthcheck anterior con depends_on, como se explica en la guía de healthchecks y políticas de reinicio; así lo probé, con un segundo servicio que usó esta cadena de conexión.

Si prefieres administrar la base de datos con una interfaz web en vez de por línea de comandos, puedes añadir un cliente como Adminer o pgweb a la misma red. Lo cubrimos en cómo administrar bases de datos con Adminer y pgweb. Y para una consulta rápida desde el propio contenedor, tienes psql a mano:

docker compose exec db psql -U app -d appdb

¿Cómo hacer copias de seguridad con pg_dump?

La regla de oro es que tus datos valen mucho más que el contenedor. El volumen pg_data guarda toda la base de datos, pero una copia lógica con pg_dump es más portátil. Según su documentación, se puede cargar en versiones mayores más nuevas, pero no hay garantía de que funcione en una más antigua. Genera un volcado en formato personalizado (comprimido y apto para restauración selectiva) directamente desde el contenedor:

docker compose exec db pg_dump -U app -Fc appdb > appdb-$(date +%F).dump

Para restaurarlo en un servidor limpio, usa pg_restore sobre la base de datos ya creada, con --if-exists junto a --clean:

cat appdb-2026-09-16.dump | docker compose exec -T db \
  pg_restore -U app -d appdb --clean --if-exists

Sin --if-exists, pg_restore intenta borrar objetos que en una base recién creada aún no existen. En mi prueba recuperó los datos, pero terminó con código 1 y el aviso warning: errors ignored on restore: 4. Con --if-exists terminó con código 0, tanto en una base vacía como encima de datos existentes, y esa misma pareja de comandos pasó una base de la 17.11 a la 18.6.

Si gestionas más de una base de datos y quieres incluir también los roles y permisos globales, usa pg_dumpall en lugar de pg_dump. Automatiza el volcado con una tarea cron y guarda las copias fuera del servidor; una copia que vive en la misma máquina que la base de datos no es una copia de seguridad de verdad.

¿Cómo ajustar el rendimiento y el healthcheck?

Con la configuración por defecto, PostgreSQL reserva un shared_buffers de 128 MB, un valor conservador pensado para arrancar en cualquier sitio. En un servidor dedicado a la base de datos con 1 GB de RAM o más, la documentación propone empezar por el 25 % de la memoria del sistema. Puedes pasar parámetros de arranque sin tocar postgresql.conf añadiendo un command al servicio:

    command: >
      postgres
      -c shared_buffers=256MB
      -c max_connections=100
      -c work_mem=16MB

El healthcheck que ya incluimos usa pg_isready, la herramienta oficial que comprueba si el servidor acepta conexiones. Con interval: 10s y retries: 5, Docker marca el contenedor como healthy en cuanto la base de datos responde, y como unhealthy si deja de hacerlo. Ese estado es justo lo que depends_on: condition: service_healthy necesita para que tus aplicaciones esperen a una base de datos realmente lista, en vez de fallar al conectar durante el arranque.

Docker lanza la primera comprobación un interval completo después de arrancar el contenedor. Por eso, en mis pruebas, el servidor aceptaba conexiones a los 3 s pero el estado healthy llegó a los 10 s. Para acortar esa espera, añade start_period y start_interval al bloque, que exigen Docker Engine 25.0 o posterior:

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
      start_interval: 1s

Con esos dos campos, docker compose up -d --wait terminó en 4,3 s en lugar de 11,6 s, con la máquina a una carga media de 24.

Preguntas frecuentes

¿Por qué no debo usar la etiqueta latest en producción?

Porque :latest sigue a la versión mayor estable más reciente (hoy es la misma imagen que 18.6, según Docker Hub) y puede arrastrarte a un salto de versión mayor en el siguiente docker compose pull. PostgreSQL 19 estaba en Beta 3 el 13 de agosto de 2026. Al fijar postgres:18.6-alpine decides tú cuándo actualizar: las menores de la 18 no exigen volcado ni restauración, así que sube el número y recrea el contenedor después de leer sus notas de versión. Un salto entre versiones mayores sí requiere pg_upgrade o un volcado y restauración, y la imagen 18 se niega a arrancar sobre un clúster de la 17 en lugar de intentarlo por su cuenta.

¿Se pierden los datos si borro el contenedor?

No, siempre que uses un volumen con nombre montado en /var/lib/postgresql como en esta guía: lo comprobé con docker compose down y up en la 18.6, con Alpine y con Debian. Solo perderías los datos con docker compose down -v, que elimina también los volúmenes. Montar la ruta antigua /var/lib/postgresql/data con la 18 no borra nada, porque el contenedor se detiene con Error: in 18+, these Docker images are configured to store database data in a format which is compatible with "pg_ctlcluster". El caso que sí los pierde es el inverso: una imagen 17 con el volumen en /var/lib/postgresql escribe en un volumen anónimo y arranca con una base vacía al recrearla.

¿Debo exponer el puerto 5432 a Internet?

No. Publicarlo como 127.0.0.1:5432:5432 lo deja accesible solo desde la propia máquina, que es lo habitual cuando la app y la base de datos conviven en el mismo servidor. Si de verdad necesitas acceso remoto, hazlo a través de una VPN o un túnel SSH y protégelo con una contraseña fuerte. Exponer PostgreSQL directamente en la red pública es una de las causas más frecuentes de compromiso de datos.

Conclusión

Con un único docker-compose.yml tienes un servidor PostgreSQL 18.6 persistente, con su volumen en la ruta que exige la 18, sus variables de entorno y un healthcheck que garantiza arranques fiables. A partir de aquí, la base de datos queda lista para dar servicio al resto de tu infraestructura autoalojada.

Conecta tus aplicaciones por el nombre del servicio y añade un administrador web con Adminer o pgweb. Sobre todo, programa copias de seguridad periódicas con pg_dump y guárdalas fuera del servidor. Esa es la diferencia entre tener una base de datos y tener una base de datos que puedes recuperar.

Fuentes

  1. Imagen oficial de PostgreSQL en Docker Hub
  2. Documentación oficial de PostgreSQL
  3. Anuncio de PostgreSQL 18 (postgresql.org)
  4. Cambio de PGDATA y volumen en la imagen (docker-library/postgres, GitHub)
  5. Anuncio de PostgreSQL 18.6 y 19 Beta 3 (postgresql.org)
  6. Política de versiones de PostgreSQL
  7. docker-entrypoint.sh de la imagen 18 (docker-library/postgres, GitHub)
  8. Notas de versión de PostgreSQL 18
  9. Documentación de pg_dump
  10. Documentación de pg_restore
  11. Parámetros de memoria de PostgreSQL 18
  12. Referencia de HEALTHCHECK (Docker Docs)

Ruta: Bases de datos y almacenamiento self-hosted con Docker