Variádico

Docker, Go, Postgres, y Nginx

21 de junio de 2015

Quería aprender cómo usar docker-compose para crear una aplicación de varios contenedores. En la documentación de Compose no hay un ejemplo de cómo crear una aplicación con Go, Postgres, y Nginx. Un componente de este problema es cómo retener o guardar datos de tu aplicación. Afortunadamente, eso ya lo resolví en otra publicación. Este guía va tratar sobre cómo crear una aplicación, contenedorizada, usando Docker, Go, para la aplicación, Postgres, para el banco de datos, y Nginx, para el proxy inverso.

Instalar Compose

Voy a suponer que ya has instalado Docker. Para este guía vas a necesitar docker-compose también. Si tienes un Mac, puedes usar Homebrew.

$ brew update
$ brew install docker-compose

Lista de contenedores

Este proyecto va a usar 4 contenedores.

Crear imagen de Go

Escribir un servidor sencillo

Vamos a poner todos los archivos en $GOPATH/src/holadocker/.

Esta apli es un servidor que escucha en puerto :8080 desde un contenedor. Cuando arranque, intentará conectarse a Postgres y creará unas tablas. Si visitas /, entonces se insertan datos al banco de datos. Inmediatamente después, se hace una petición para así poder verificar si se están guardando los datos.

// holadocker/servidor.go

package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"

	_ "github.com/lib/pq"
)

var (
	db *sql.DB
)

func main() {
	bancoInfo := fmt.Sprintf(
		"user=postgres dbname=postgres password=%s host=%s port=%s sslmode=disable",
		"mypass",
		os.Getenv("DB_PORT_5432_TCP_ADDR"),
		os.Getenv("DB_PORT_5432_TCP_PORT"),
	)

	var err error
	db, err = sql.Open("postgres", bancoInfo)
	if err != nil {
		log.Fatal(err)
	}

	if err = db.Ping(); err != nil {
		log.Fatal(err)
	}

	_, err = db.Exec(
		`create table if not exists mydata (
			id serial primary key,
			val integer not null
		)`)
	if err != nil {
		log.Fatal(err)
	}

	http.HandleFunc("/", servirInicio)
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

func servirInicio(res http.ResponseWriter, pet *http.Request) {
	res.Header().Set("Content-Type", "text/plain; charset=utf-8")
	fmt.Fprintln(res, "¡Hola, mundo!\n")

	fmt.Fprintln(res, "DB_ADDR:", os.Getenv("DB_PORT_5432_TCP_ADDR"))
	fmt.Fprintln(res, "DB_PORT:", os.Getenv("DB_PORT_5432_TCP_PORT"))

	_, err := db.Exec("insert into mydata(val) values(0)")
	if err != nil {
		log.Fatal(err)
	}

	filas, err := db.Query("select id from mydata")
	if err != nil {
		log.Fatal(err)
	}

	for filas.Next() {
		var id int

		err = filas.Scan(&id)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Fprintf(res, "ID: %d\n", id)
	}
}

Quizá estés pensando, «¿de dónde sacó os.Getenv("DB_PORT_5432_TCP_ADDR")?». Ten paciencia. Eso lo explicaré en breve.

Escribir un Dockerfile

Lo que hace el Dockerfile es copiar el código fuente Go a una imagen, descargar las dependencias, y compilar tu código. Si quieres saber lo que hace :onbuild, puedes leer el Dockerfile de Go aquí.

# holadocker/Dockerfile

FROM golang:onbuild
EXPOSE 8080

Preparar configuración de Nginx

Vamos a crear una archivo de configuración básico para Nginx. Este lo copié de un contenedor de Nginx y nada más añadí el bloque de server.

# holadocker/nginx.conf

user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;

    # añadido por mí
    server {
        listen 80;
        proxy_pass_header Server;

        location / {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;

            # apli es definido en /etc/hosts automáticamente por Docker
            proxy_pass http://apli:8080;
        }
    }
}

Crear archivo de Compose

Por último, tenemos que describir todos los servicios que va a usar nuestra aplicación. Los servicios y enlaces entre contenedores se describen en un archivo llamado docker-compose.yml. Te recomiendo que leas todos los comentarios que escribí en este archivo para que entiendas lo que está haciendo. ¿Te acuerdas que te confundiste con os.Getenv("DB_PORT_5432_TCP_ADDR")? En este archivo está la explicación.

# holadocker/docker-compose.yml

almacen:
    # contenedor de datos
    image: postgres:latest # reusar imagen de postgres
    volumes:
        - /var/lib/postgresql/data
    command: true

postgres:
    image: postgres:latest
    ports:
        - "5432" # elegir puerto disponible al azar
    volumes_from:
        - almacen # conectar postgres al contenedor de datos
    environment:
        - POSTGRES_PASSWORD=mypass
        # establece el usuario y nombre del banco de datos, si quieres...

go:
    build: .
    links:
        - postgres:db # [otro contenedor]:[alias del contenedor en este]
        # esto creará variables de entorno en el contenedor de go
        # con información del ip y el puerto del contenedor de postgres
        # también crea una entrada en /etc/hosts
    ports:
        - "8080" # exponiendo éste puerto en el contenedor

proxy:
    image: nginx:latest
    ports:
        - "80:80" # anfitrión:contenedor
        - "443:443"
    volumes:
        - nginx.conf:/etc/nginx/nginx.conf:ro
        # conectar nuestro nginx.conf con el del contenedor
        # :ro denomina sólo permisos de lectura al contenedor
    links:
        - go:apli # [otro contenedor]:[alias del contenedor en este]
        # esto creará variables de entorno en el contenedor de nginx
        # con información del ip y el puerto del contenedor de go
        # también crea una entrada en /etc/hosts

Construir y ejecutar

Ahora que tenemos los 4 archivos listos, podemos ejecutar nuestra apli de varios contenedores.

$ pwd
$GOPATH/src/holadocker/
$ docker-compose build
$ docker-compose up -d

Si acaso ves un error que se parece a «Cannot start container 8675309: Cannot link to a non running container», entonces introduce docker-compose up -d de nuevo. (No sé por qué pasa eso…)

Si todo se hizo bien, entonces podrás ir a tu navegador para verificar que tu aplicación esté en ejecución. Usa el comando boot2docker ip para conocer la dirección de tu aplicación. Recuerda que expusiste el puerto :8080.

Muy largo; no leí

Instala docker-compose. Copia estos archivos a tu Mac.

Ponlos en una carpeta llamada holadocker/ y entra a esa carpeta. Introduce el comando docker-compose up -d para crear una aplicación de varios contenedores.