Etiqueta: docker

  • Un ejemplo de Laravel y React sobre Docker que funciona

    Un ejemplo de Laravel y React sobre Docker que funciona

    Cuando me llegó este mensaje

    Necesito aprender a preparar entorno de desarrollo php, laravel y react con Docker, ¿tendrás algún ejemplo para recomendarme?

    Mi primer instinto fue buscar algún tutorial para compartir. ¿Qué otra cosa podría ser mejor, no?

    Y ahí me metí a hurgar en GitHub y probar unas cuantas opciones pero ninguna parecía funcionar.

    Al menos no sin hacer unos cuantos ajustes y, dado que mi framework de referencia no es Laravel si no Symfony, me pareció que lo más lógico era seguir buscando.

    Pues bien, hurgando por aquí y por allí llegué a este tutorial que me dio (casi) justo lo que estaba buscando.

    Se trata de una sencilla app de manejo de usuarios:

    Lo interesante: está desarrollada usando Laravel y React:

    Y montada sobre Docker.

    Si querés ver los detalles de cómo logré levantar la aplicación te recomiendo que leas el tutorial original aunque debo hacer una advertencia: tuve que hacer pequeños ajustes al código para dejarla funcionando pero, ahora que está ahí, podés descargar el código del repositorio y ejecutando estos comandos desde la raíz del proyecto:

    1. docker compose up -d --wait (O su primo hermano vendor/bin/sail up -d --wait)
    2. npm run dev

    Deberías ser capaz de entrar a http://localhost y ser recibido por la aplicación.

    El objetivo de este post no es analizar el propio código (Ni PHP ni React), si no centrarme en cómo se montó todo esto sobre Docker.

    Para comprender lo que te voy a mostrar a continuación necesitás conocer los conceptos básicos de Docker. Si no los tenés frescos te recomiendo que, antes de seguir, busques aprender cómo funciona Docker en general (Tal vez este post sea un mejor lugar para arrancar).

    ¿Avanzamos?

    Los archivos de docker

    Primero que nada, una aclaración: Estos archivos los generó sail.

    No es que eso tenga nada de malo, simplemente comentar que, si usás Laravel, no parece una mala idea apoyarte en este wrapper.

    Ahora sí, el primer archivo a analizar no es otro que el docker-compose.yml

    services:
        laravel.test:
            build:
                context: './vendor/laravel/sail/runtimes/8.4'
                dockerfile: Dockerfile
                args:
                    WWWGROUP: '${WWWGROUP}'
            image: 'sail-8.4/app'
            extra_hosts:
                - 'host.docker.internal:host-gateway'
            ports:
                - '${APP_PORT:-80}:80'
                - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
            environment:
                WWWUSER: '${WWWUSER}'
                LARAVEL_SAIL: 1
                XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
                XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
                IGNITION_LOCAL_SITES_PATH: '${PWD}'
            volumes:
                - '.:/var/www/html'
            networks:
                - sail
            depends_on:
                - mysql
        mysql:
            image: 'mysql/mysql-server:8.0'
            ports:
                - '${FORWARD_DB_PORT:-3306}:3306'
            environment:
                MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ROOT_HOST: '%'
                MYSQL_DATABASE: '${DB_DATABASE}'
                MYSQL_USER: '${DB_USERNAME}'
                MYSQL_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ALLOW_EMPTY_PASSWORD: 1
            volumes:
                - 'sail-mysql:/var/lib/mysql'
                - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
            networks:
                - sail
            healthcheck:
                test:
                    - CMD
                    - mysqladmin
                    - ping
                    - '-p${DB_PASSWORD}'
                retries: 3
                timeout: 5s
    networks:
        sail:
            driver: bridge
    volumes:
        sail-mysql:
            driver: local
    

    No te preocupes si te resulta algo intimidante, suele ser así cuando se ven por primera vez. Intentaré desmenuzarlo para que sea más sencillo el consumo.

    Lo primero que encontramos es que hay dos servicios: laravel.test y mysql.

    Analizaré cada uno por separado

    El servicio laravel.test

    Lo primero que se ve en la definición de este servicio es la clave build, lo que significa que se utilizará una imagen custom para crear el respectivo contenedor:

            build:
                context: './vendor/laravel/sail/runtimes/8.4'
                dockerfile: Dockerfile
                args:
                    WWWGROUP: '${WWWGROUP}'

    A partir de esta definición se pueden extraer las siguientes conclusiones:

    1. Se utilizará el archivo Dockerfile ubicado en vendor/laravel/sail/runtimes/8.4
    2. El proceso de construcción de la imagen requiere un argumento llamado WWWGROUP
    3. El valor del argumento de build se extrae de una variable de contexto llamada WWWGROUP

    Veamos el contenido del Dockerfile para hacernos una idea más clara de qué tendrá la imagen y cómo se hará la construcción:

    El archivo vendor/laravel/sail/runtimes/8.4/Dockerfile

    FROM ubuntu:24.04
    
    LABEL maintainer="Taylor Otwell"
    
    ARG WWWGROUP
    ARG NODE_VERSION=22
    ARG MYSQL_CLIENT="mysql-client"
    ARG POSTGRES_VERSION=17
    
    WORKDIR /var/www/html
    
    ENV DEBIAN_FRONTEND=noninteractive
    ENV TZ=UTC
    ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
    ENV SUPERVISOR_PHP_USER="sail"
    
    RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
    
    RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
        echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
        echo "Acquire::BrokenProxy    true;" >> /etc/apt/apt.conf.d/99custom
    
    RUN apt-get update && apt-get upgrade -y \
        && mkdir -p /etc/apt/keyrings \
        && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano  \
        && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
        && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
        && apt-get update \
        && apt-get install -y php8.4-cli php8.4-dev \
           php8.4-pgsql php8.4-sqlite3 php8.4-gd \
           php8.4-curl php8.4-mongodb \
           php8.4-imap php8.4-mysql php8.4-mbstring \
           php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \
           php8.4-intl php8.4-readline \
           php8.4-ldap \
           php8.4-msgpack php8.4-igbinary php8.4-redis \
    #       php8.4-swoole \
           php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \
        && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
        && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
        && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
        && apt-get update \
        && apt-get install -y nodejs \
        && npm install -g npm \
        && npm install -g pnpm \
        && npm install -g bun \
        && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
        && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
        && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
        && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
        && apt-get update \
        && apt-get install -y yarn \
        && apt-get install -y $MYSQL_CLIENT \
        && apt-get install -y postgresql-client-$POSTGRES_VERSION \
        && apt-get -y autoremove \
        && apt-get clean \
        && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
    
    RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4
    
    RUN userdel -r ubuntu
    RUN groupadd --force -g $WWWGROUP sail
    RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
    
    COPY start-container /usr/local/bin/start-container
    COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
    COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini
    RUN chmod +x /usr/local/bin/start-container
    
    EXPOSE 80/tcp
    
    ENTRYPOINT ["start-container"]

    Creo que comentar línea por línea este archivo sería un poco demasiado así que me voy a centrar en las partes más relevantes:

    1. La imagen base que se usa es ubuntu:24.04 (Link acá)
    2. Es posible especificar, además de WWWGROUP, la versión de NodeJS, el cliente de MySQL y la versión de PostgreSQL
    3. La versión de PHP que se utiliza es la 8.4
    4. Hay una cantidad de extensiones php instaladas (sqlite3, gd, mongodb, readline, etc…)
    5. XDebug viene instalado

    Respecto del argumento obligatorio WWWGROUP, se usa para establecer el id del grupo al que pertenecerá el usuario sail (el encargado de levantar el proceso php dentro del contenedor).

    El objetivo de esto es evitar los problemas de permisos que podría haber cuando Laravel requiera escribir sobre archivos que deben ser accedidos por el servidor web.

    El valor de este argumento lo provee sail.

    Este Dockerfile es, por definición, bastante genérico, para ir a producción te convendría revisarlo bien.

    En mi caso, por ejemplo, sólo utilizo MySQL, con lo cual, todo lo relativo a PostgreSQL y MongoDB no lo necesito.

    A continuación vemos

            image: 'sail-8.4/app'

    Lo que indica que la imagen que se generará llevará el nombre de sail-8.4/app

            extra_hosts:
                - 'host.docker.internal:host-gateway'

    Esta definición agrega la capacidad de conectarse desde dentro del contenedor hacia el host, fundamental para que xdebug funcione.

            ports:
                - '${APP_PORT:-80}:80'
                - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'

    En esta sección se establecen los mapeos entre los puertos del host y los del contenedor, de modo de que al acceder a localhost:{$APP_PORT} y localhost:{$VITE_PORT} se pueda realizar peticiones al servidor web y al vite respectivamente.

    Los -80 y -5173 son los valores por defecto, es decir, los que se usarán a menos que haya otros especificados en variables de entorno (o en archivos .env).

            environment:
                WWWUSER: '${WWWUSER}'
                LARAVEL_SAIL: 1
                XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
                XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
                IGNITION_LOCAL_SITES_PATH: '${PWD}'

    Esta parte define algunas variables de entorno que estarán presentes cuando el contenedor se levante.

    El uso específico de cada una de ellas depende de lo que hagan los procesos instalados en el contenedor (el webserver principalmente).

            volumes:
                - '.:/var/www/html'

    Esta sección define el punto de montaje del código de la aplicación hacia el directorio /var/www/html dentro del contenedor.

            networks:
                - sail

    Aquí se define la red a la que estará conectado el contenedor (sail).

            depends_on:
                - mysql

    Y por último, se define una dependencia entre el servicio laravel.test y mysql, lo que en la práctica quiere decir que el orden de inicio de los servicios será primero mysql y luego laravel.test.

    El servicio mysql

    El segundo servicio es para la base de datos, en mi caso MySQL:

            image: 'mysql/mysql-server:8.0'
            ports:
                - '${FORWARD_DB_PORT:-3306}:3306'
            environment:
                MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ROOT_HOST: '%'
                MYSQL_DATABASE: '${DB_DATABASE}'
                MYSQL_USER: '${DB_USERNAME}'
                MYSQL_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ALLOW_EMPTY_PASSWORD: 1
            volumes:
                - 'sail-mysql:/var/lib/mysql'
                - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
            networks:
                - sail
            healthcheck:
                test:
                    - CMD
                    - mysqladmin
                    - ping
                    - '-p${DB_PASSWORD}'
                retries: 3
                timeout: 5s

    A simple vista podés notar que no hay una clave build, lo que significa que se usará una imagen estándar, en este caso mysql/mysql-server:8.0 (Link acá)

    A través de la siguiente definición:

            environment:
                MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ROOT_HOST: '%'
                MYSQL_DATABASE: '${DB_DATABASE}'
                MYSQL_USER: '${DB_USERNAME}'
                MYSQL_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ALLOW_EMPTY_PASSWORD: 1

    Se establecen algunos valores necesarios para la incialización del servidor de base de datos.

            volumes:
                - 'sail-mysql:/var/lib/mysql'
                - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'

    Aquí se monta el volumen nombrado sail-mysql al directorio /var/lib/mysql del contenedor, con el objetivo de lograr persistencia de los datos.

    Mediante el segundo mapeo se consigue crear la base de datos como puedes ver en vendor/laravel/sail/database/mysql/create-testing-database.sh:

    #!/usr/bin/env bash
    
    mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
        CREATE DATABASE IF NOT EXISTS testing;
        GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
    EOSQL

    Aquí se define la pertenencia de este contenedor a la misma red (sail) que se utiliza en el servicio laravel.test para garantizar la conectividad entre ambos

            networks:
                - sail

    Y mediante esta configuración:

            healthcheck:
                test:
                    - CMD
                    - mysqladmin
                    - ping
                    - '-p${DB_PASSWORD}'
                retries: 3
                timeout: 5s

    Se establece un comando capaz de detectar si el servicio está funcional o no para tomar alguna acción al respecto o, al menos, poder informar de esta situación.

    Finalmente nos encontramos con la definición de las redes:

    networks:
        sail:
            driver: bridge

    Y los volúmenes nombrados:

    volumes:
        sail-mysql:
            driver: local

    Qué hacer con todo esto

    Este post, y el código que lo acompaña, están armados como un recurso didáctico, con lo cual, el mejor uso que les podés dar es estudiarlos a fondo.

    Tu tarea pues es:

    1. Descargar el código
    2. Levantar la aplicación localmente
    3. Estudiar los archivos de Docker
    4. Hacer algún pequeño cambio, por ejemplo agregar phpMyAdmin
    5. Si te animás, agregar alguna funcionalidad, ya sea al frontend o al backend
  • Cómo instalar extensiones PHP en Docker

    Cómo instalar extensiones PHP en Docker

    Estás arrancando la dockerización de tu aplicación PHP.

    Como para probar un poco, levantaste un contenedor usando un comando:

    docker run -v $(pwd):/app -it php:latest index.php

    Y ahí nomás te encontraste con el primero de los problemas:

    PHP Fatal error: Uncaught Error: Class "ZipArchive" not found

    ¡Claro!

    Tu aplicación necesita de la extensión Zip para funcionar… ¿Y ahora?

    Bueno, a no desesperar.

    Tenés varias opciones disponibles:

    1. Buscar una imagen que ya tenga la extensión zip instalada
    2. Agregar las instrucciones para que se compile e instale la extensión durante el build de la imagen
    3. Usar un script de instalación

    Para el resto de los ejemplos tomaré una versión hiper simplificada de lo que puede ser tu aplicación:

    <?php
    
    $zip = new ZipArchive();
    if ($zip->open($argv[1]) == TRUE) {
     for ($i = 0; $i < $zip->numFiles; $i++) {
         echo $zip->getNameIndex($i).PHP_EOL;
     }
    }

    Y asumiré que tienes un archivo comprimido llamado compressed.zip.

    Una imagen Docker PHP con la extensión zip

    Seguramente habrá muchas opciones, una de ellas, como para salir del paso, es thecodingmachine/php:7.3-v4-slim-cli (Podés encontrar otras similares acá).

    Para usarla este comando puede ser útil: docker run -v $(pwd):/usr/src/app --rm -it thecodingmachine/php:7.3-v4-slim-cli php index.php compressed.zip

    Como decía, esta solución no es ni de lejos la mejor.

    ¿Por qué? Básicamente porque tenés muy poco control sobre lo que tiene la imágen. En otras palabras, no sabés muy bien qué otras cosas te estás trayendo además de la extensión zip.

    En el mejor de los casos vas a terminar con una imagen más grande de lo necesario.

    En el peor vas a incluir extensiones que no necesitás y exponerte a riesgos de seguridad.

    Mejor seguir leyendo.

    Compilar la extensión zip dentro del build de la imagen

    Una forma de contar con la extensión zip (o cualquier otra en realidad) en tu contenedor es compilar php de modo que lo tenga incorporado.

    Por ejemplo, usando un Dockerfile como este:

    FROM ubuntu:latest
    
    ADD https://www.php.net/distributions/php-8.4.1.tar.gz /php/php.tar.gz
    RUN apt-get update && \
            apt-get install -y libzip-dev build-essential pkg-config libxml2-dev libsqlite3-dev && \
            tar xzf /php/php.tar.gz -C /php/ && \
            cd /php/php-8.4.1/ && \
            ./configure --prefix=/usr/local/php-8.4.1 --with-zip && \
            make && make install && \
            ln -s /usr/local/php-8.4.1/bin/php /usr/local/bin
    WORKDIR /app

    Y construtendo la imagen usando docker build . -t php-zip

    Podrías ejecutar tu script usando: docker run -v $(pwd):/app --rm -it php-zip php index.php compressed.zip

    Y no verás el error que estaba al comienzo.

    Esta opción, si bien es más correcta que la primera, puede resultar un poco overkill. Particularmente, si tenés que instalar varias extensiones, llegar al Dockerfile correcto puede ser bastante laborioso.

    En la siguiente sección te doy la que personalmente recomiendo

    Un script de instalación de extensiones PHP para Docker

    Bueno, en realidad no es un script de instalación si no dos.

    Arranco por el más conocido: php-ext-install.

    Lo bueno de este script es que, si partís de una imagen oficial de php, no tenés que hacer nada para tenerlo disponible.

    Lo malo de este script es que requiere bastante ayuda para hacer lo suyo.

    Por ejemplo, instalar la extensión zip implica agregar a tu Dockerfile:

    RUN docker-php-ext-install zip

    Pero… con eso sólo no alcanza. Si lo intentas, al hacer el build verás este error:

    Package 'libzip', required by 'virtual:world', not found
    Package 'libzip', required by 'virtual:world', not found
    Package 'libzip', required by 'virtual:world', not found

    Lo que quiere decir que necesitas, antes de esto, instalar la librería zip.

    En definitiva, el Dockerfile que te dará lo que buscas es uno que se parezca a:

    FROM php:8.4.1-cli
    
    RUN apt update && apt-get install -y libzip-dev && docker-php-ext-install zip

    En este caso no ha sido tan terrible, cierto, pero hay otras extensiones que también requieren ciertos cambios en configuraciones. Nada del otro mundo pero es algo más que hay que recordar.

    El script que me gusta más, y el que recomiendo usar, es docker-php-extension-installer.

    A diferencia del primero, docker-php-extension-installer debe ser instalado explícitamente pero, una vez instalado, es mucho más cómodo.

    Aquí un ejemplo de Dockerfile para la extensión zip:

    FROM php:8.4.1-cli
    
    ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
    
    RUN install-php-extensions zip
    
    WORKDIR /app

    Nuevamente, ejecutando docker run -v $(pwd):/app --rm -it php-zip php index.php compressed.zip verás el listado de los archivos contenidos en el archivo comprimido.

    Así que ya lo sabés: la próxima vez que te toque agregar extensiones a un php sobre docker, agregá esta línea a tu Dockerfile:

    ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/

    Y problema resuelto.

  • ¿Cuántos contenedores  necesita tu php?

    ¿Cuántos contenedores necesita tu php?

    Cuando trabajabas con máquinas virtuales no había dudas: cuanto más completa sea la máquina mejor.

    Sí, instalarla por primera vez era un trabajito. Que definir el hardware, el disco, el sistema operativo, instalarl Apache, MySQL, Git… una mañana se te iba en un abrir y cerrar de ojos. Pero funcionaba.

    Luego apareció Vagrant y fue como tocar el cielo con las manos… por un tiempo.

    Ahora te estás queriendo pasar a Docker y el saber popular dice que hay que tener muchos contenedores pequeños en lugar de un único monolito que tenga todo lo necesario para correr la app.

    ¿Por qué esa es la mejor opción?

    Dejame que te plantee la pregunta al revés: ¿por qué es una buena idea meter todo en una única máquina virtual?

    Tal vez te parezca ridículo pero seguime el juego por un momento.

    La respuesta es obvia, ¿no?: ¡porque te quedás sin máquina en un segundo!

    Ahora bien, si tuvieras memoria infinita y un procesador que no se agota nunca… ¿no preferirías tener una VM por cada proceso?

    Imaginate, un Apache en una VM, el MySQL en otra, tal vez un Redis por allá, una VM para correr los cronjobs…

    Tan mal no estaría, ¿verdad?

    Bueno, tal vez sería compleja la orquestación de todo eso, pero tampoco sería tan terrible.

    De esa forma, cada VM podría tener su propio sistema operativo, sus propias dependencias instaladas y, en caso de que algo falle, sería sólo ese servicio el que se vería afectado.

    De pronto no suena tan mal, ¿o sí?

    Precisamente esa es la idea de usar Docker (O, más correctamente, contenedores): tener diferentes ambientes de ejecución auto-contenidos.

    Pero… ¿no vas a caer en el mismo problema de correr muchas VMs en una misma computadora?

    No. Acá es donde la cosa se pone más técnica pero digamos que la diferencia principal entre una VM y un contenedor es que la VM virtualiza hardware mientras que el contenedor sólo virtualiza software.

    En la práctica, esto quiere decir que el contenedor es mucho más liviano que una VM.

    La contracara es que el contenedor te da algo menos de flexibilidad.

    Por ejemplo, es muy fácil tener corriendo una VM con Windows y otra con Linux en un host Mac. Hacer lo mismo con contenedores… no tanto.

    Ahora bien, si tus contenedores usan el mismo sistema operativo de base (Por ejemplo son todos Linux), en un mismo host podés albergar significativamente más contenedores que VMs.

    La siguiente pregunta es… administrar tantos contenedores ¿no se vuelve complejo?

    Y… la respuesta es que un poco sí.

    Ahí es donde aparece docker-compose para salvar el día. Tema para otro post.

  • Scripts de CLI: ¿dentro o fuera de Docker?

    Scripts de CLI: ¿dentro o fuera de Docker?

    Tenés una aplicación web montada sobre Docker.

    Cuando accedés usando el navegador todo funciona a las mil maravillas.

    Está todo listo para ir a producción.

    O casi.

    Existen algunas pequeñas tareas que hay que hacer por fuera de la web. Limpiar archivos viejos… borrar las cuentas de usuario inactivas… lo típico, bah.

    Qué mejor para esto que un script de CLI, ¿no?

    Así que, ahí fuiste a codearlo.

    Y ahora, hay que probarlo:

    php my_script.php

    No funciona.

    ¿Qué pasó?

    La conexión a la base de datos falla.

    ¿Cómo es posible?

    A ver qué dice el archivo .env:

    DB_HOST="db"
    DB_NAME="my_db"
    DB_USER="my_user"
    DB_PASS="my_pass"

    Nada extraño por aquí.

    El archivo de conexión:

    <?php
    
    $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
    $dotenv->load();
    $conn = new PDO("mysql:dbname={$_ENV['DB_NAME']};host={$_ENV['DB_HOST'}", $_ENV['DB_USER'], $_ENV['DB_PASS'] );

    Por aquí tampoco hay nada raro…

    ¡Momento!

    Si la URL a la que ingresás es http://localhost:8080… el host de la db ¿no debería ser localhost?

    Probar no cuesta mucho… cambiás db por localhost y… ¡funciona!

    Listo, vamos a producción.

    mmm, mejor hacemos una última prueba del sitio, ¿no?

    Boom. Error 500.

    PHP Fatal error:  Uncaught PDOException: SQLSTATE[HY000] [2002] No such file or directory in...

    ¿Cómo es posible? Si recién funcionaba…

    ¿Es que acaso es imposible hacer funcionar la web y el script de CLI sobre Docker?

    No, claro que no.

    Veámoslo paso a paso.

    Qué pasa cuando se accede vía web

    Cuando accedés a tu aplicación a través de la url http://localhost:8080, a pesar de que diga localhost, no es realmente tu computadora la que atiende esa petición (Bueno técnicamente sí lo es, pero a través de Docker).

    El :8080 juega un papel muy importante.

    Para que esto funcione, el puerto 8080 debe estar mapeado al puerto 80 del webserver que está corriendo en tu contenedor Docker.

    Esto significa que, si bien tu computadora está escuchando a través del puerto 8080, lo que hace es re-enviar todo el tráfico recibido a través de él hacia el puerto 80 dentro del contenedor Docker configurado a tal efecto (Probablemente esto está definido en el archivo docker-compose.yml).

    Distinto sería el caso si estuvieses usando el servidor incorporado a php (Es decir, si iniciaras tu aplicación vía php -S localhost:8080). En tal caso, la web y el CLI estarían usando el mismo entorno y, por lo tanto, no tendrías problemas.

    Veamos ahora qué es lo que ocurre en el otro caso.

    Qué pasa cuando se accede vía CLI

    Lo primero que debés comprender es qué es exactamente lo que se ejecuta cuando hacés php my_script.php.

    Ante todo, estás invocando al intérprete de php pasándole como argumento la cadena my_script.php.

    Hasta aquí supongo que no hay nada muy novedoso, ¿cierto?

    El problema comienza cuando tu script depende de configuraciones de entorno, como en este ejemplo.

    Lo que ocurre es que, precisamente, el entorno de tu host es diferente del de los contenedores Docker. De eso se tratan los contenedores: de unidades de ejecución aisladas del host.

    De hecho, Docker tiene su propio manejo de redes interno.

    Esto quiere decir que el nombre db dentro del contenedor está asociado con una IP, mientras que fuera del entorno Docker no.

    Cómo solucionar el problema

    Ahora que tenés claro por dónde pasa el problema, la solución es ejectuar el script de PHP dentro del contenedor.

    Tenés varias formas de hacerlo. Te comento rápidamente dos de ellas:

    Con docker-compose

    Si estás usando docker-compose podés ejecutar el siguiente comando:

    docker-compose exec my_service php my_script.php

    Suponiendo que el servicio donde está tu script está activo y se llama my_service, lo que verás en pantalla será el resultado de la ejecución de tu script.

    Sin docker-compose

    Si no usás docker-compose podés usar un comando del estilo de:

    docker exec -v $(pwd):/var/www my_container php my_script.php

    Este ejemplo es muy similar al de arriba. La principal diferencia es que en lugar de referirte a un servicio en ejecución, tenés que referirte directamente a un contenedor (my_container en este ejemplo) y tenés que montar los volúmenes en forma explícita.

    Si el contenedor está corriendo el resultado que obtendrás será el mismo que el anterior.

    Un pequeño consejo

    Algo que puede ayudarte a evitar este tipo de situaciones es eliminar el php de tu host.

    De esta forma, no vas a tener más opción que ejecutarlo dentro de Docker cuando así lo requieras.

    Es cierto, es una opción algo extremista pero aún así puede resultarte útil ya que de esta forma siempre estarás seguro de que la versión de php que está utilizando tu script es exactamente la que esperás.

  • Cómo actualizar la versión de php que tiene una imagen docker

    Cómo actualizar la versión de php que tiene una imagen docker

    ¿Alguna vez te pasó algo como esto?

    me he bajado una imagen que contiene wordpress con letsencrypt, pero la versión de php que utiliza es la 5.6 y necesito actualizarla a la 7.2.
    ¿Hay alguna forma de modificar esto desde dentro o desde fuera del contenedor?

    Qué dilema, ¿no?

    La primera pregunta que se me ocurre es ¿realmente te bajaste una imágen y no un Dockerfile?

    Por un rato asumiré que así es… después lo volvemos a revisar.

    Así que, teniendo esta imagen podés construir contenedores pero… ¿si hacés la actualización y el contenedor se destruye? ¡Olvidate de la actualización! No suena muy divertido, ¿no? ¿Y entonces? ¿¿No hay salida??

    Bueno… no tan rápido.

    Un comando de Docker no muy usado (ni muy comentado honestamente) es el comando commit el cual puede venir bastante bien en esta situación.

    Este comando permite crear una imagen basada en el estado actual de un contenedor. Es decir, es una forma de crear una imagen sin tener un Dockerfile.

    En este hipotético escenario se podría hacer algo del estilo:

    docker run -it wordpress:php5.6 bash

    Y, una vez dentro, realizar la actualización siguiendo algún tutorial como este por ejemplo.

    El problema, o mejor dicho, el primero de los problemas, es que esta versión de php es tan vieja que las imágenes que la contienen suelen estar basadas en versiones igual de viejas el sistema operativo. Esto implica que los repositorios ya quedaron obsoletos… en fin, que hacer esta actualización será un viajecito en sí mismo (Sí, te lo digo por experiencia después de haber probado unas 10 imágenes tratando de armar un ejemplo para este post).

    Pero bueno… supongamos que, de alguna forma, te las arreglaste para actualizar el php.

    Mientras el contenedor no se elimine, podrás usar docker ps -a para obtener algo como:

    Y, una vez obtenido el ID del contenedor:

    docker commit 9d50869ba044 wordpress:php5.6.muc

    Esto te creará una nueva imagen para usar en tu sistema con la cual podrás crear un nuevo contenedor, por ejemplo:

    docker run -it wordpress:php5.6.muc bash

    Y ahora sí, al hacer php -v encontrarás la versión de php que necesitabas.

    ¡Genial! ¿No? Bueno… no exactamente.

    Para empezar, la nueva versión de php no necesariamente será compatible con tu versión de WordPress… es más, lo más probable es que ese no sea el caso.

    Aún si lo fuera… ¿vale la pena ponerse a actualizar la versión de PHP? La verdad es que lo veo bastante poco práctico.

    Precisamente, la idea de usar Docker es crear contenedores desechables. ¿Quedó desactualizada la versión de php? Ningún problema, armemos una nueva imagen con la versión que se se requiere y listo.

    De hecho, lo más probable es que lo que hayas descargado sea un Dockerfile más que una mera imagen.

    Si ese es el caso, podrías hacer algo tan sencillo como modificar la imagen usada como base y re-construir la imagen.

    Es decir, si el Dockerfile se ve algo así como:

     # Base image
    FROM php:5.6-apache
    
    # Install the mysqli extension
    RUN docker-php-ext-install mysqli
    
    # Update repo
    RUN apt-get update -y
    
    # Install mysql-client
    RUN apt-get install mysql-client -y
    
    # Install wp-cli as wp
    RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar  && \
      chmod +x wp-cli.phar && \
      mv wp-cli.phar /usr/local/bin/wp
    
    # Download and extract WordPress files on /var/www/html
    RUN wp core download --allow-root
    
    # Get args from docker-composer.yml
    ARG WORDPRESS_DB_NAME
    ARG WORDPRESS_DB_USER
    ARG WORDPRESS_DB_PASSWORD
    ARG WORDPRESS_DB_HOST
    
    # Creating wp-config.php file on /var/www/html
    RUN wp config create --dbname=${WORDPRESS_DB_NAME} --dbuser=${WORDPRESS_DB_USER} --dbpass=${WORDPRESS_DB_PASSWORD} --dbhost=${WORDPRESS_DB_HOST} --allow-root --skip-check
    
    COPY entrypoint.sh /entrypoint.sh
    
    # makes the script executable
    RUN chmod +x /entrypoint.sh
    
    ENTRYPOINT ["/entrypoint.sh"]
    
    CMD ["apache2-foreground"]

    Podrías cambiarlo por

     # Base image
    FROM php:7.2-apache
    
    # Install the mysqli extension
    RUN docker-php-ext-install mysqli
    
    # Update repo
    RUN apt-get update -y
    
    # Install mysql-client
    RUN apt-get install mysql-client -y
    
    # Install wp-cli as wp
    RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar  && \
      chmod +x wp-cli.phar && \
      mv wp-cli.phar /usr/local/bin/wp
    
    # Download and extract WordPress files on /var/www/html
    RUN wp core download --allow-root
    
    # Get args from docker-composer.yml
    ARG WORDPRESS_DB_NAME
    ARG WORDPRESS_DB_USER
    ARG WORDPRESS_DB_PASSWORD
    ARG WORDPRESS_DB_HOST
    
    # Creating wp-config.php file on /var/www/html
    RUN wp config create --dbname=${WORDPRESS_DB_NAME} --dbuser=${WORDPRESS_DB_USER} --dbpass=${WORDPRESS_DB_PASSWORD} --dbhost=${WORDPRESS_DB_HOST} --allow-root --skip-check
    
    COPY entrypoint.sh /entrypoint.sh
    
    # makes the script executable
    RUN chmod +x /entrypoint.sh
    
    ENTRYPOINT ["/entrypoint.sh"]
    
    CMD ["apache2-foreground"]

    Y ejecutar nuevamente docker build.

    Ojo: ¡Esto no resuelve el problema de la incompatibilidad de la versión de PHP y de WordPress! Pero al menos te ahorra unos cuantos dolores de cabeza tratando de hacer la migración.

    Mi consejo al final sería pasar todos los datos a volúmenes, buscar una imagen que tenga las versiones del software que querés usar y con esa imagen crear un nuevo contenedor donde montarlos.

    Probablemente eso sea mucho más sencillo y eficiente.

  • Cómo agregar extensiones PHP a una imagen Docker

    Cómo agregar extensiones PHP a una imagen Docker

    ¿Tu aplicación necesita generar algún gráfico usando GD? ¿O tal vez algo más común como parsear un XML?

    Muchas de las funciones de bajo nivel de php están disponibles a través de extensiones que probablemente no se encuentren instaladas en tu imagen base.

    ¿Qué puedes hacer?

    Las opciones son varias:

    1. Buscar una nueva imagen base que sí las tenga instaladas
    2. Descargar el código fuente, compilar e instalar manualmente las extensiones
    3. Usar el script que viene con las imágenes oficiales de php

    Supongo que la elección es clara, ¿no?

    Cómo usar docker-php-ext-install

    En general, si usás una imagen basada en alguna de las imágenes oficiales de php, vas a tener acceso a este simpático script.

    Para usarlo basta con invocarlo de esta forma:

    docker-php-ext-install <EXTENSION>

    Por ejemplo, para instalar la extensión zip se puede usar:

    docker-php-ext-install zip

    Es probable que, en algunas imágenes, esto genere un error debido a la falta de las librerías de base requeridas, en el caso de zip se trata de libzip-dev.

    Para resolver este problema se requiere, previa a la invocación del script docker-php-ext-install, realizar la instalación correspondiente, por ejemplo usando:

    apt install -y libzip-dev

    Claro que, si esto se hace dentro de un contenedor en ejecución este cambio será efímero, es decir, si el contendor se destruye habrá que repetir todo el proceso.

    Cómo persistir el cambio en la imagen

    Lo mejor, si se pretende que el cambio persista más allá de la vida del contenedor en particular, es realizar estos cambios en el Dockerfile.

    Algo así como:

    FROM php:7.4.33-apache
    
    LABEL authors="Mauro Chojrin <mauro.chojrin@leewayweb.com>"
    
    RUN apt update && \
        apt install -y libzip-dev && \
        docker-php-ext-install zip

    De esta forma, al realizar un build, la imagen resultante incluirá la extensión que se requiere y, por lo tanto, cualquier contenedor que la utilice también la tendrá disponible.

  • Cómo ejecutar phpMyAdmin en Docker

    Cómo ejecutar phpMyAdmin en Docker

    Una vez tienes dockerizada tu aplicación, lo siguiente que querrás hacer, como para terminar de dejar atrás el viejo XAMPP, será acceder a tu base de datos en forma gráfica, ¿cierto?.

    Asumiré que tienes un archivo docker-compose.yml similar a este:

    version: '3.8'
    services:
      db:
        image: 'mysql:5.7.42-debian'
        environment:
          - MYSQL_ROOT_PASSWORD=root
          - MYSQL_DATABASE=ruko
          - MYSQL_USER=ruko
          - MYSQL_PASSWORD=ruko
        restart: always
        volumes:
          - 'db-data:/var/lib/mysql'
      webserver:
        image: 'ruko_dev'
        user: 'www-data'
        build:
          context: '.'
        restart: always
        ports:
          - '8888:80'
        volumes:
          - './app/:/var/www/html/'
    volumes:
      db-data: {}

    Con lo cual, al hacer docker-compose up:

    • Tendrás acceso a tu sitio a través de http://localhost:8888
    • Cada cambio que realices se reflejará automáticamente al recargar la página

    Lo que faltaría sería poder ingresar a alguna dirección local (Por qué no http://localhost:9999) y ver algo como:

    Como de costumbre, existen varias opciones para lograrlo. La que considero más conveniente es crear un nuevo contenedor que pueda ejecutar phpMyAdmin. Veámoslo.

    phpMyAdmin en un contenedor nuevo

    Básicamente, lo que se necesita es un nuevo contenedor que contenga un servidor web y pueda conectarse a la base de datos que tiene tu entorno.

    Qué mejor para ello que usar una imagen estándar de phpMyAdmin, ¿no?

    En otras palabras, basta con ejecutar un comando del estilo:

    docker run --name phpmyadmin -d --network dockerized_rukovoditel_342_default -p 9999:80 phpmyadmin

    La red dockerized_rukovoditel_342_default corresponde a la red creada durante la ejecución de docker-compose up en mi ambiente, en el tuyo seguramente será algo diferente. En todo caso, siempre puedes usar docker network ls para averiguarlo.

    Con esto ya tenemos lo suficiente como para ingresar a http://localhost:9999 y trabajar cómodamente con phpMyAdmin.

    Claro que tener que usar este comando no es lo más práctico del mundo, ¿no?

    Completemos el trabajo metiendo la nueva configuración dentro del docker-compose.yml.

    phpMyAdmin en un contenedor nuevo manejado por docker-compose

    Como podrás imaginar, sólo se trata de traducir la llamada directa al cliente de docker a la sintaxis YAML utilizada por docker-compose. Es decir, se trata de sumar esta definición a la sección services del archivo docker-compose.yml:

      pma:
        image: 'phpmyadmin'
        ports:
          - '9999:80'
        restart: always

    Para probarlo todo basta con ejecutar docker-compose down y docker-compose up.

  • ¿Cómo cambiar el upload_max_file_size desde el Dockerfile?

    ¿Cómo cambiar el upload_max_file_size desde el Dockerfile?

    Que tiempos aquellos en que el tamaño los archivos que se subían vía HTTP se medía en KB, ¿no? Qué fácil era la vida por entonces.

    Pero bueno… el mundo avanza y con él la necesidad de compartir nuestras fotos en alta calidad se hace presente.

    En ese contexto, tener un upload máximo de 2 MB puede convertirse en un gran limitante.

    Y, casualmente, el tamaño máximo por defecto del upload_max_filesize de PHP es de… 2 MB.

    En un ambiente de desarrollo normal modificar esto es fácil, ¿no?

    Basta con:

    1. Ejecutar el comando php --ini
    2. Abrir el archivo referido en Configuration File (php.ini)
    3. Buscar la línea upload_max_filesize = 2M
    4. Modificarla por un valor acorde a nuestras necesidades (Por ejemplo upload_max_filesize => 10M)
    5. Reiniciar el servidor (o php-fpm)

    Pero si estamos usando un contenedor Docker las cosas no son tan simples.

    Es decir, hacer el cambio en sí no presenta una dificultad especial:

    docker exec -it 7b8da0fb9892 bash
    apt update
    apt install -y vim
    vim /usr/local/etc/php/php.ini

    Modificar el valor de la configuración, guardar y listo.

    El problema es que… apenas el contenedor se destruya, el cambio se fue con él y estamos otra vez donde empezamos.

    Es todo una cuestión de imagen

    La solución basada en el Dockerfile pasa por tener, dentro de la imagen que se construya, una definición del php.ini que contenga los cambios que necesitamos.

    ¿Cómo lograr eso? Pues se trata de tener, dentro de nuestro proyecto, nuestro propio php.ini y meterlo dentro de la imagen durante el build.

    En concreto, supongamos que partimos de un Dockerfile que se ve así:

    FROM php:7.4-apache
    
    LABEL authors="Mauro Chojrin <mauro.chojrin@leewayweb.com>"
    
    RUN apt-get update && \
        apt-get install -y \
            libpng-dev \
            libzip-dev
    
    RUN docker-php-ext-install gd && \
        docker-php-ext-install zip && \
        docker-php-ext-install mysqli
    
    ADD app /var/www/html

    Con esto podemos crear una imagen para luego levantar un contenedor con un Apache que podrá procesar php para responder a las peticiones.

    En este ejemplo, el código (El contenido del directorio /var/www/html dentro del contenedor) será el que se encuentre dentro del directorio app de nuestro proyecto.

    Lo que toca hacer es agregar un archivo php.ini dentro del proyecto e indicar a Docker que lo agregue a la imagen durante el build:

    FROM php:7.4-apache
    
    LABEL authors="Mauro Chojrin <mauro.chojrin@leewayweb.com>"
    
    RUN apt-get update && \
        apt-get install -y \
            libpng-dev \
            libzip-dev
    
    RUN docker-php-ext-install gd && \
        docker-php-ext-install zip && \
        docker-php-ext-install mysqli
    
    ADD app /var/www/html
    
    ADD php.ini /usr/local/etc/php/

    ¿Qué debería contener el archivo php.ini? Si queremos asegurarnos de que sólo se modifique el upload_max_filesize necesitamos empezar por un archivo que contenga la configuración presente dentro del contenedor.

    Cómo obtener el archivo original

    Lo primero que podemos probar es buscar el archivo dentro del contenedor1:

    docker exec -it 7b8da0fb9892 php --ini

    Esto dará un resultado similar a:

    Configuration File (php.ini) Path: /usr/local/etc/php
    Loaded Configuration File:         (none)
    Scan for additional .ini files in: /usr/local/etc/php/conf.d
    Additional .ini files parsed:      /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini

    Si tuviera un archivo lo copiaría usando:

    docker cp 7b8da0fb9892:/usr/local/etc/php/php.ini .

    Pero, como en mi caso me indica que el archivo de configuración cargado es ninguno, tendré que probar otra cosa.

    Lo que haré será crear en el directorio del proyecto un pequeño script llamado dump_php_ini.php:

    <?php 
    $s = []
    foreach(ini_get_all() as $k=>$v) {
         if ($v['local_value'] || $v['global_value']) {
            $s[] = sprintf("%s = %s", $k, $v['local_value']?$v['local_value']:$v['global_value']);
        }
    }
    
    echo join("\n", $s);

    Y luego ejecutar este comando:

    docker run -v $(pwd):/var/www/html -it php:7.4-apache php dump_php_ini.php > php.ini

    Con eso tengo el archivo que contiene la configuración exacta que se está usando actualmente.

    Y ahora sí, puedo editarlo, hacer la modificación que se requiere y volver a construir la imagen usando:

    docker build . -t my_apache_php

    A partir de aquí, todos los contenedores que sean creados usando este Dockerfile tendrán la configuración deseada y los visitantes del sitio podrán compartir sus fotos sin inconvenientes.

    1. Cambia el ID del contenedor por el tuyo ↩︎
  • ¿Cómo direccionar un dominio a un contenedor Docker?

    ¿Cómo direccionar un dominio a un contenedor Docker?

    Tenés tu entorno local funcionando perfecto con Docker.

    Llevar la imagen al hosting y crear el contenedor no es un problema pero, cuando querés que los clientes vean el sitio… ¿qué les tenés que pasar? ¿No sería genial poder pasarles una URL con tu propio dominio?

    Veamos qué necesitarías para hacerlo.

    1. Un servidor donde alojar tus contenedores
    2. Un dominio que puedas direccionar a tu servidor
    3. La configuración de DNS
    4. La configuración del webserver dentro de Docker

    Asumiré que los puntos 1, 2 y 3 los tenés resueltos y me concentraré en el punto 4.

    Cómo configurar Docker para que responda a un dominio único

    Si se trata de un único contenedor corriendo en el servidor no es necesario hacer demasiado. Basta con asegurarse de que el contenedor esté conectado al puerto 80 del servidor.

    Por ejemplo, si se inicia en un servidor un contenedor usando algo como:

    docker run --restart=always -p  80:80 imagen -d

    Será suficiente para dejar el contenedor corriendo en background en forma constante.

    Con una configuración estándar de Apache como por ejemplo:

    <VirtualHost *:80>
    	# The ServerName directive sets the request scheme, hostname and port that
    	# the server uses to identify itself. This is used when creating
    	# redirection URLs. In the context of virtual hosts, the ServerName
    	# specifies what hostname must appear in the request's Host: header to
    	# match this virtual host. For the default virtual host (this file) this
    	# value is not decisive as it is used as a last resort host regardless.
    	# However, you must set it for any further virtual host explicitly.
    	ServerName www.example.com
    
    	ServerAdmin webmaster@localhost
    	DocumentRoot /var/www/html
    
    	# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
    	# error, crit, alert, emerg.
    	# It is also possible to configure the loglevel for particular
    	# modules, e.g.
    	#LogLevel info ssl:warn
    
    	ErrorLog ${APACHE_LOG_DIR}/error.log
    	CustomLog ${APACHE_LOG_DIR}/access.log combined
    
    	# For most configuration files from conf-available/, which are
    	# enabled or disabled at a global level, it is possible to
    	# include a line for only one particular virtual host. For example the
    	# following line enables the CGI configuration for this host only
    	# after it has been globally disabled with "a2disconf".
    	#Include conf-available/serve-cgi-bin.conf
    </VirtualHost>
    
    # vim: syntax=apache ts=4 sw=4 sts=4 sr noet

    Es suficiente.

    En definitiva, casi que es más difícil lograr que el contenedor no quede asociado al dominio que lo contrario.

    Cómo configurar Docker para que responda a diferentes dominios

    Distinto, y más interesante, es el caso de querer alojar en un mismo servidor varios sitios diferentes, cada uno con su propio contenedor.

    Para lograr eso se requiere algo más de maña a nivel de infraestructura.

    Un webserver como Apache permite, por ejemplo, usar múltiples hosts virtuales (VirtualHost) pero, obviamente, el puerto 80 del servidor es uno solo. Esto quiere decir que hay un único proceso escuchando en el puerto 80 y, de todo el tráfico que se recibe a través de él, es su responsabilidad decidir cómo responder.

    Para que a cada sitio le llegue el tráfico que le corresponde, y sólo ese tráfico, es necesario definir algún tipo de mapeo entre la petición que recibe el webserver y el host virtual que lo atenderá.

    Una forma de hacer esto es asociar diferentes hosts a diferentes puertos.

    <VirtualHost *:80>
    	...
    </VirtualHost>
    <VirtualHost *:81>
    	...
    </VirtualHost>
    <VirtualHost *:82>
    	...
    </VirtualHost>

    Claro que, si la idea es que a todos los sitios se acceda a través del puerto estándar, esta opción queda descartada.

    Otra opción disponible es desambiguar por IP:

    <VirtualHost 172.21.0.1:80>
    	...
    </VirtualHost>
    <VirtualHost 137.184.122.59:80>
    	...
    </VirtualHost>
    <VirtualHost 93.184.216.34:80>
    	...
    </VirtualHost>

    Esto puede funcionar si hay más de una IP pública asociada al servidor.

    Lo más común, sin embargo, es desambiguar a través del ServerName:

    <VirtualHost *:80>
    	ServerName dominio1.com
    </VirtualHost>
    <VirtualHost *:80>
    	ServerName dominio2.com
    </VirtualHost>
    <VirtualHost *:80>
    	ServerName dominio3.com
    </VirtualHost>

    En este esquema, Apache determinará qué VirtualHost es el requerido analizando la URL. Un request de tipo http://dominio1.com será atendido siguiendo la primera definción, http://dominio2.com la segunda y http://dominio3.com la tercera.

    El primer paso es claro: definir un VirtualHost por cada dominio que se quiera usar.

    Segundo paso: conectar el tráfico del host al contenedor que le corresponde.

    Para lograr esto es necesario contar con el módulo proxy de Apache. En el caso de Ubuntu basta con el comando sudo a2enmod proxy_http; sudo service apache2 restart para tenerlo todo listo.

    Luego, por cada sitio que se quiera servir, se debe crear la configuración del VirtualHost:

    <VirtualHost *:80>
        ServerName dominio1.com
        ProxyPass / http://127.0.0.1:5000/
        ProxyPassReverse / http://127.0.0.1:5000/
    </VirtualHost>

    De esta forma, todo el tráfico que se reciba a través de http://dominio1.com será redirigido automáticamente hacia el puerto 5000 dentro del mismo servidor (De ahí el 127.0.0.1).

    Por último, para cerrar el círculo se necesita contar con un contenedor Docker escuchando en el puerto seleccionado, en este caso, el 5000. Con esto será suficiente para la prueba.

    docker run -p 5000:80 -v $(pwd):/var/www/html php:7.4-apache

    Asumiendo que el directorio actual contiene un archivo como este:

    <?php
    
    echo phpversion();

    Al abrir el navegador en http://dominio1.com/ se verá algo como:

    Si aún no tenés los dominios apuntados al servidor o querés hacer pruebas en tu entorno local basta con que modifiques tu archivo /etc/hosts apuntando todos los dominios a 127.0.0.1.

    El caso del segundo dominio será prácticamente igual:

    <VirtualHost *:80>
        ServerName dominio2.com
        ProxyPass / http://127.0.0.1:5001/
        ProxyPassReverse / http://127.0.0.1:5001/
    </VirtualHost>

    La diferencia está en el puerto. El contenedor para el segundo dominio deberá estar escuchando a un puerto diferente en el host, pero éste deberá estar mapeado al puerto 80 dentro del contenedor. Por ejemplo:

    docker run -p 5001:80 -v $(pwd):/var/www/html php:8.2-apache

    Y, nuevamente, al abrir el navegador en http://dominio2.com verás:

    Y así continúa por cada uno de los dominios que necesites apuntar

  • ¿Puede Docker sustituir a XAMPP?

    ¿Puede Docker sustituir a XAMPP?

    Hace unos años, montar un entorno de desarrollo para una aplicación web PHP era una verdadera molestia:

    Primero era necesario instalar Apache, luego MySQL, después configurar todo para que el Apache pudiera procesar correctamente los archivos PHP y generar la salida esperada, pasando en el medio por la configuración de MySQL y rogar que todo funcionara bien.

    Y así pasábamos los días hasta que llegó XAMPP.

    Qué es XAMPP

    XAMPP fue, en su día, una verdadera revolución: un paquete único que contenía todo lo que podíamos necesitar para desarrollar cómodamente: click aquí, click allí y listo, a programar se ha dicho!

    Si bien XAMPP simplicaba, y mucho, el montaje de un entorno local, en el fondo no se trataba de algo esencialmente diferente de lo que veníamos haciendo hasta el momento… sólo que mucho más rápido y cómodo.

    El problema empezó cuando muchos programadores comenzaron a usarlo como si se tratara de una solución mágica y dejaron de lado comprender qué es lo que estaba ocurriendo tras bambalinas.

    Craso error.

    Error que se hace patente a la hora de subir el sitio al hosting. Es entonces cuando empiezan los problemas como:

    Tengo este código que en localhost funciona perfectamente, que recibe los datos enviados mediante un formulario y saca los resultados mediante una tabla que se rellena automáticamente según los datos enviados. Resulta que cuando lo subo al servidor (hosting) no funciona y no da ningún tipo de error, simplemente no dibuja la tabla.

    O:

    tengo una pagina en php, en el servidor local funciona bien, es un login que al ingresar los datos dirige a una parte especifica de la web dependiendo el perfil, ahora si subo esto a mi hosting funciona perfectamente, pero si subo la web a otro dominio al ingresar los datos del login se queda la página en blanco. Ya intente que mostrara errores pero no me sale nada.

    Hace algunos años habría recomendado usar máquinas virtuales para desarrollar. Hoy por hoy, existiendo Docker hay un nuevo ganador.

    Qué es Docker

    Docker es, ante todo, una plataforma de ejecución de aplicaciones basada en el concepto de contenedor.

    La idea, muy resumida, es tener una única unidad (un paquete) que contenga en sí mismo todo lo necesario para ejecutar una aplicación.

    De este modo, nos olvidamos de las pequeñas sorpresas que pueden encontrarse al llevar a un hosting un proyecto desarrollado en forma local.

    ¿Puede usarse Docker para desarrollos PHP?

    ¡Claro que sí!

    Existen varias formas diferentes de usar Docker en proyectos PHP. En general, lo que se necesitará será una imagen que incluya el intérprete de PHP y, si se trata de una aplicación web, también un servidor.

    XAMPP vs. Docker

    Recapitulando un poco, como para que quede claro: XAMPP es un paquete de software específicamente diseñado para PHP, mientras que Docker es un sistema de manejo de contenedores genérico, es decir, no sólo puede usarse para PHP si no para cualquier lenguaje.

    Por otro lado, con Docker es sumamente sencillo tener, en una misma máquina física, muchos procesos PHP corriendo diferentes versiones. Te reto a que intentes eso con XAMPP.

    La desventaja, por llamarle de algún modo, de usar Docker en lugar de XAMPP es su dificultad para montarlo. Claro que, una vez que lo domines ésta desaparecerá.

    ¿Cómo reemplazar XAMPP por Docker?

    Para reemplazar XAMPP por Docker se requiere contar con una configuración de Docker que incluya los servicios que XAMPP contiene (Además del propio Docker instalado localmente, claro).

    En principio se trata de:

    Esto supone crear 4 contenedores diferentes, los cuales tendrán que estar en sincronía.

    Suena complicado, ¿no? Bueno… no es para tanto.

    Usando docker-compose es posible lograrlo con poco esfuerzo.

    Por ejemplo, si tenemos un proyecto donde tenemos un archivo llamado index.php que contiene lo siguiente:

    <?php
    
    echo "Viva PHP!";

    Se puede:

    1. Crear un archivo docker-compose.yml en el mismo directorio con este contenido:
    version: '3.8'
    services:
        mariadb:
            image: 'mariadb:11.0'        
            volumes:
                - 'database-data:/var/lib/mysql'
            environment:
                - MYSQL_ROOT_PASSWORD=root
                - MYSQL_DATABASE=my_app
                - MYSQL_USER=my_user
                - MYSQL_PASSWORD=my_pwd
            ports:
                - '29003:3306'
            restart: always
        webserver:
            image: 'php:7.4-apache'
            working_dir: '/var/www/html'
            volumes:
                - '.:/var/www/html'
            ports:
                - '29000:80'
            restart: always   
        pma:
            image: 'phpmyadmin:latest'
            ports:
                - '29005:80'
            links:
                - 'mariadb:db'
            environment:
                - PMA_ARBITRARY=1
    volumes:
        database-data: {}
    1. Darle al docker-compose up -d
    1. Abrir el navegador en http://localhost:29000 y voilá:

    Luego, si se quiere entrar al PhpMyAdmin basta con abrir una nueva ventana en http://localhost:29005, ingresar las credenciales y se verá esto:

    Y poco más…

    A partir de aquí es posible hacer todo lo que se hacía con XAMPP, con la diferencia de que luego subir todo al hosting será mucho más sencillo.