"¡Necesitamos DevOps!"(la frase más popular al final de cualquier hackathon)
Primero, algunas letras.
Cuando un desarrollador es un excelente desarrollador que puede implementar su creación en cualquier máquina bajo cualquier OC, esto es una ventaja. Sin embargo, si no entiende nada más que su IDE, esto no es un inconveniente: al final, se le paga por el código y no por la capacidad de implementarlo. Un especialista estrecho y profundo en el mercado se valora más que la habilidad promedio de "jack of all trades". Para las personas como nosotros, "usuarios IDE", a las buenas personas se les ocurrió Docker.
El principio de Docker es el siguiente: "funciona para mí, funciona en todas partes". El único programa necesario para implementar una copia de su aplicación en cualquier lugar es Docker. Si ejecuta su aplicación en la ventana acoplable en su máquina, se garantiza que se ejecutará con el mismo éxito en cualquier otra ventana acoplable. Y nada más que un acoplador necesita ser instalado. Por ejemplo, ni siquiera tengo Java en el servidor virtual.
¿Cómo funciona Docker?
Docker crea una imagen de una máquina virtual con aplicaciones instaladas en ella. Además, esta imagen se desarrolla como una máquina virtual completamente autónoma. Una copia en ejecución de la imagen se denomina "contenedor". Puede ejecutar cualquier cantidad de imágenes en el servidor, y cada una de ellas será una máquina virtual separada con su propio entorno.
¿Qué es una máquina virtual? Esta es la ubicación encapsulada en el servidor con el sistema operativo en el que están instaladas las aplicaciones. En cualquier sistema operativo, una gran cantidad de aplicaciones generalmente están girando, en el nuestro hay una.
El esquema de despliegue del contenedor se puede representar de la siguiente manera:

Para cada aplicación, creamos nuestra propia imagen y luego implementamos cada contenedor por separado. Además, puede colocar todas las aplicaciones en una imagen e implementarlas como un contenedor. Además, para no desplegar cada contenedor por separado, podemos usar una utilidad docker-compose separada, que configura los contenedores y la relación entre ellos a través de un archivo separado. Entonces la estructura de toda la aplicación puede verse así:

No contribuí intencionalmente con la base de datos al ensamblaje general de Docker, por varias razones. En primer lugar, la base de datos es completamente independiente de las aplicaciones que funcionan con ella. Puede estar lejos de ser una aplicación, pueden ser solicitudes manuales de la consola. Personalmente, no veo ninguna razón para hacer que la base de datos dependa del ensamblado Docker en el que se encuentra. Por lo tanto, lo soporté. Sin embargo, a menudo se practica un enfoque en el que la base de datos se coloca en una imagen separada y se lanza en un contenedor separado. En segundo lugar, quiero mostrar cómo interactúa el contenedor Docker con sistemas fuera del contenedor.
Sin embargo, bastante la letra, vamos a escribir el código. Escribiremos la aplicación más simple en primavera y reaccionaremos, lo que registrará nuestras llamadas al frente en la base de datos, y plantearemos todo esto a través de Docker. La estructura de nuestra aplicación se verá así:

Hay muchas formas de implementar dicha estructura. Estamos implementando uno de ellos. Crearemos dos imágenes, lanzaremos dos contenedores a partir de ellas, además, el back-end se conectará a la base de datos, que está instalada en un servidor específico en algún lugar de Internet (sí, tales solicitudes a la base de datos no se enviarán rápidamente, pero no nos impulsa la sed de optimización, pero interés científico).
La publicación se dividirá en partes:
0. Instalar Docker.
1. Escribimos aplicaciones.
2. Recopilamos imágenes y lanzamos contenedores.
3. Recopile imágenes e inicie contenedores en un servidor remoto.
4. Resolver problemas de red.
0. Instalar Docker
Para instalar Docker, debe ir al
sitio y seguir lo que está escrito allí. Al instalar Docker en un servidor remoto, prepárese para el hecho de que Docker puede no funcionar con servidores en OpenVZ. Además, puede haber problemas si no tiene habilitadas las iptables. Es recomendable iniciar el servidor en KVM con iptables. Pero estas son mis recomendaciones. Si todo funciona para usted, y así, me alegrará que no haya pasado mucho tiempo descubriendo por qué no funciona, cómo tuve que hacerlo.
1. Escribimos aplicaciones
Escribamos una aplicación simple con el backend más primitivo en Spring Boot, una interfaz muy simple en ReactJS y una base de datos MySQL. La aplicación tendrá una página única con un solo botón que registrará la hora en que se hizo clic en ella en la base de datos.
Espero que ya sepa cómo escribir aplicaciones en el arranque, pero si no, puede clonar el proyecto terminado. Todos los enlaces al final del artículo.
Backend en Spring Boot
build.gradle:
plugins { id 'org.springframework.boot' version '2.1.4.RELEASE' id 'java' } apply plugin: 'io.spring.dependency-management' group = 'ru.xpendence' version = '0.0.2' sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'mysql:mysql-connector-java' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
Entidad de registro:
package ru.xpendence.rebounder.entity; import com.fasterxml.jackson.annotation.JsonFormat; import javax.persistence.*; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Objects; @Entity @Table(name = "request_logs") public class Log implements Serializable { private Long id; private LocalDateTime created; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long getId() { return id; } @Column @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") public LocalDateTime getCreated() { return created; } @PrePersist public void prePersist() { this.created = LocalDateTime.now(); }
LogController, que funcionará en lógica simplificada y escribirá inmediatamente en la base de datos. Omitimos el servicio.
package ru.xpendence.rebounder.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ru.xpendence.rebounder.entity.Log; import ru.xpendence.rebounder.repository.LogRepository; import java.util.logging.Logger; @RestController @RequestMapping("/log") public class LogController { private final static Logger LOG = Logger.getLogger(LogController.class.getName()); private final LogRepository repository; @Autowired public LogController(LogRepository repository) { this.repository = repository; } @GetMapping public ResponseEntity<Log> log() { Log log = repository.save(new Log()); LOG.info("saved new log: " + log.toString()); return ResponseEntity.ok(log); } }
Todo, como vemos, es muy simple. Mediante una solicitud GET, escribimos en la base de datos y devolvemos el resultado.
Discutiremos el archivo de configuración de la aplicación por separado. Hay dos de ellos.
application.yml:
spring: profiles: active: remote
application-remote.yml:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://my-remote-server-database:3306/rebounder_database?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC username: admin password: 12345 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate.dialect: org.hibernate.dialect.MySQL5Dialect server: port: 8099
Cómo funciona esto, probablemente sepa que Spring primero escanea el archivo application.properties o application.yml, cuál encuentra. En él indicamos una configuración única: qué perfil usaremos. Por lo general, durante el desarrollo, acumulo varios perfiles, y es muy conveniente cambiarlos utilizando el perfil predeterminado. A continuación, Spring encuentra application.yml con el sufijo deseado y lo usa.
Especificamos la fuente de datos, la configuración de JPA y, lo que es más importante, el puerto externo de nuestro backend.
Frontend ReactJS
También puede ver el frontend en un proyecto en git, o incluso no puede verlo, sino clonarlo y ejecutarlo.
Puede verificar el trabajo individual de la interfaz descargando el proyecto, yendo a la carpeta raíz del proyecto en la terminal (donde se encuentra el archivo package.json) y ejecutando dos comandos en secuencia:
npm install // , maven npm start //
Por supuesto, para esto necesita el Node Package Manager (npm) instalado, y esta es la forma más difícil de evitar usar Docker. Si aún comenzó el proyecto, verá la siguiente ventana:

Oh, bueno, es hora de mirar el código. Solo indicaré la parte que se refiere al backend.
export default class Api { _apiPath = 'http://localhost:8099'; _logUrl = '/log'; getResource = async () => { const res = await fetch(`${this._apiPath}${this._logUrl}`); if (!res.ok) { throw new Error(`Could not fetch ${this._logUrl}` + `, received ${res.status}`) } return await res.json(); }; };
La interfaz funciona de manera predecible. Seguimos el enlace, esperamos la respuesta y la mostramos en la pantalla.

Vale la pena centrarse en los siguientes puntos:
- El frente está abierto al mundo exterior a través del puerto 3000. Este es el puerto predeterminado para React.
- La parte posterior se abre en el puerto 8099. Lo configuramos en la configuración de la aplicación.
- La parte posterior está golpeando la base de datos a través de Internet externo.
La aplicación está lista.
2. Recoge imágenes y lanza contenedores
La estructura de nuestra asamblea será la siguiente. Crearemos dos imágenes: frontend y backend, que se comunicarán entre sí a través de puertos externos. Para la base, no crearemos una imagen, la instalaremos por separado. Por qué ¿Por qué no creamos una imagen para la base? Tenemos dos aplicaciones que cambian constantemente y no almacenan datos en nosotros mismos. La base de datos almacena datos en sí misma, y esto puede ser el resultado de varios meses de operación de la aplicación. Además, a esta base de datos se puede acceder no solo desde nuestra aplicación de fondo, sino también por muchas otras, para eso también es una base de datos y no la volveremos a armar constantemente. Nuevamente, esta es una oportunidad para trabajar con una API externa, que, por supuesto, es conectarse a nuestra base de datos.
Ensamblaje frontal
Para ejecutar cada aplicación (ya sea frontal o posterior), necesita una cierta secuencia de acciones. Para ejecutar la aplicación en React, debemos hacer lo siguiente (siempre que ya tengamos Linux):
- Instalar NodeJS.
- Copie la aplicación a una carpeta específica.
- Instale los paquetes necesarios (comando de instalación npm).
- Inicie la aplicación con el comando npm start.
Es esta secuencia de acciones la que tenemos que realizar en la ventana acoplable. Para hacer esto, en la raíz del proyecto (en el mismo lugar donde se encuentra package.json), debemos colocar el Dockerfile con el siguiente contenido:
FROM node:alpine WORKDIR /usr/app/front EXPOSE 3000 COPY ./ ./ RUN npm install CMD ["npm", "start"]
Veamos qué significa cada línea.
FROM node:alpine
Con esta línea, le dejamos en claro a la ventana acoplable que cuando inicia el contenedor, lo primero que debe hacer es descargar Docker desde el repositorio e instalar NodeJS, y la más liviana (todas las versiones más livianas de marcos y bibliotecas populares en Docker generalmente se llaman alpinas).
WORKDIR /usr/app/front
En el contenedor de Linux, se crearán las mismas carpetas estándar que en otras Linux: / opt, / home, / etc, / usr, etc. Establecemos el directorio de trabajo con el que trabajaremos - / usr / app / front.
EXPOSE 3000
Abrimos el puerto 3000. A través de este puerto se producirá más comunicación con la aplicación que se ejecuta en el contenedor.
COPY ./ ./
Copie el contenido del proyecto de origen en la carpeta de trabajo del contenedor.
RUN npm install
Instale todos los paquetes necesarios para ejecutar la aplicación.
CMD ["npm", "start"]
Iniciamos la aplicación con el comando npm start.
Este escenario se ejecutará en nuestra aplicación cuando se inicie el contenedor.
Vamos a aclarar el frente. Para hacer esto, en la terminal, estando en la carpeta raíz del proyecto (donde se encuentra el Dockerfile), ejecute el comando:
docker build -t rebounder-chain-frontend .
Valores de comando:
docker es una llamada a la aplicación docker, bueno, ya lo sabes.
construir: crea una imagen a partir de materiales de destino.
-t <nombre>: en el futuro, la aplicación estará disponible mediante la etiqueta especificada aquí. Puede omitir esto, entonces Docker generará su propia etiqueta, pero será imposible distinguirla de otras.
. - indica que necesita recopilar el proyecto de la carpeta actual.

Como resultado, el ensamblaje debe terminar con el texto:
Step 7/7 : CMD ["npm", "start"] ---> Running in ee0e8a9066dc Removing intermediate container ee0e8a9066dc ---> b208c4184766 Successfully built b208c4184766 Successfully tagged rebounder-chain-frontend:latest
Si vemos que el último paso se ha completado y todo ha sido exitoso, entonces tenemos una imagen. Podemos verificar esto ejecutándolo:
docker run -p 8080:3000 rebounder-chain-frontend
El significado de este comando, creo, se entiende generalmente, con la excepción de la entrada -p 8080: 3000.
docker run rebounder-chain-frontend - significa que estamos lanzando una imagen de docker, que llamamos rebounder-chain-frontend. Pero dicho contenedor no tendrá una salida al exterior, necesita establecer un puerto. Es el equipo de abajo el que lo establece. Recordamos que nuestra aplicación React se ejecuta en el puerto 3000. El comando -p 8080: 3000 le dice al acoplador que tome el puerto 3000 y lo reenvíe al puerto 8080 (que estará abierto). Por lo tanto, una aplicación que se ejecuta en el puerto 3000 se abrirá en el puerto 8080 y estará disponible en la máquina local en ese puerto.
, : Mac-mini-Vaceslav:rebounder-chain-frontend xpendence$ docker run -p 8080:3000 rebounder-chain-frontend > rebounder-chain-frontend@0.1.0 start /usr/app/front > react-scripts start Starting the development server... Compiled successfully! You can now view rebounder-chain-frontend in the browser. Local: http://localhost:3000/ On Your Network: http://172.17.0.2:3000/ Note that the development build is not optimized. To create a production build, use npm run build.
No dejes que el registro te moleste
Local: http://localhost:3000/ On Your Network: http://172.17.0.2:3000/
Reaccionar piensa que sí. Realmente está disponible dentro del contenedor en el puerto 3000, pero reenviamos este puerto al puerto 8080, y desde el contenedor la aplicación se ejecuta en el puerto 8080. Puede ejecutar la aplicación localmente y verificar esto.
Entonces, tenemos un contenedor listo para usar con una aplicación front-end, ahora vamos a recoger el backend.
Construir back-end.
El script para iniciar una aplicación en Java es significativamente diferente del ensamblado anterior. Se compone de los siguientes elementos:
- Instale la JVM.
- Recopilamos el archivo jar.
- Lo lanzamos
En Dockerfile, este proceso se ve así:
El proceso de ensamblar una imagen con la inclusión de un dzharnik en algunos puntos se parece al de nuestro frente.
El proceso de ensamblar y lanzar la segunda imagen es esencialmente el mismo que ensamblar y lanzar la primera.
docker build -t rebounder-chain-backend . docker run -p 8099:8099 rebounder-chain-backend
Ahora, si tiene ambos contenedores ejecutándose y el backend está conectado a la base de datos, todo funcionará. Le recuerdo que debe registrar la conexión a la base de datos desde el back-end usted mismo, y debe funcionar a través de una red externa.
3. Recopile imágenes y ejecute contenedores en un servidor remoto
Para que todo funcione en un servidor remoto, necesitamos que Docker ya esté instalado en él, después de lo cual, simplemente ejecute las imágenes. Seguiremos el camino correcto y enviaremos nuestras imágenes a nuestra cuenta en la nube de Docker, después de lo cual estarán disponibles desde cualquier parte del mundo. Por supuesto, hay muchas alternativas a este enfoque, así como todo lo que se describe en la publicación, pero empujemos un poco más y hagamos bien nuestro trabajo. Malo, como dijo Andrei Mironov, siempre tenemos tiempo para hacerlo.
Crear una cuenta en el Docker Hub
Lo primero que debe hacer es obtener una cuenta en el centro Docker. Para hacer esto, vaya al
centro y regístrese. No es dificil.
A continuación, debemos ir a la terminal e iniciar sesión en Docker.
docker login
Se le pedirá que ingrese un nombre de usuario y contraseña. Si todo está bien, aparecerá una notificación en el terminal que indica que el inicio de sesión se realizó correctamente.
Confirmación de imágenes en el Docker Hub
A continuación, debemos etiquetar nuestras imágenes y enviarlas al centro. Esto lo realiza el equipo de acuerdo con el siguiente esquema:
docker tag /_:
Por lo tanto, debemos especificar el nombre de nuestra imagen, inicio de sesión / repositorio y la etiqueta con la que nuestra imagen se comprometerá en el centro.
En mi caso, se veía así:

Podemos verificar la presencia de esta imagen en el repositorio local usando el comando:
Mac-mini-Vaceslav:rebounder-chain-backend xpendence$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE xpendence/rebounder-chain-backend 0.0.2 c8f5b99e15a1 About an hour ago 141MB
Nuestra imagen está lista para comprometerse. Comprometerse:
docker push xpendence/rebounder-chain-backend:0.0.2
Debería aparecer un registro de confirmación exitoso.
Haga lo mismo con la interfaz:
docker tag rebounder-chain-frontend xpendence/rebounder-chain-frontend:0.0.1 docker push xpendence/rebounder-chain-frontend:0.0.1
Ahora, si vamos a hub.docker.com, veremos dos imágenes bloqueadas. Que están disponibles desde cualquier lugar.


Felicidades Solo tenemos que pasar a la parte final de nuestro trabajo: lanzar imágenes en un servidor remoto.
Ejecutar imágenes en un servidor remoto
Ahora podemos ejecutar nuestra imagen en cualquier máquina con Docker completando solo una línea en la terminal (en nuestro caso, necesitamos ejecutar secuencialmente dos líneas en terminales diferentes, una para cada imagen).
docker run -p 8099:8099 xpendence/rebounder-chain-backend:0.0.2 docker run -p 8080:3000 xpendence/rebounder-chain-frontend:0.0.1
Este lanzamiento tiene, sin embargo, uno menos. Cuando se cierra el terminal, el proceso finalizará y la aplicación dejará de funcionar. Para evitar esto, podemos ejecutar la aplicación en modo "separado":
docker run -d -p 8099:8099 xpendence/rebounder-chain-backend:0.0.2 docker run -d -p 8080:3000 xpendence/rebounder-chain-frontend:0.0.1
Ahora la aplicación no emitirá un registro en el terminal (esto puede, nuevamente, configurarse por separado), pero incluso cuando el terminal esté cerrado, no dejará de funcionar.
4. Resolviendo problemas de red
Si hiciste todo bien, puedes esperar la mayor decepción en el camino para seguir esta publicación; bien puede resultar que nada funcione. Por ejemplo, todo funcionó perfectamente para usted y funcionó en la máquina local (como, por ejemplo, en mi Mac), pero cuando se implementó en un servidor remoto, los contenedores dejaron de verse (como, por ejemplo, en mi servidor remoto en Linux). Cual es el problema Pero el problema es este, y al principio lo insinué. Como se mencionó anteriormente, cuando se inicia el contenedor, Docker crea una máquina virtual separada, lanza Linux allí y luego instala la aplicación en ese Linux. Esto significa que el host local condicional para el contenedor en ejecución se limita al contenedor en sí y la aplicación no tiene conocimiento de la existencia de otras redes. Pero necesitamos:
a) los contenedores se vieron entre sí.
b) el servidor vio la base de datos.
Hay dos soluciones al problema.
1. Crear una red interna.
2. Traer contenedores al nivel del host.
1. En el nivel de Docker, puede crear redes, además, tres de ellas de forma predeterminada:
bridge ,
none y
host .
Bridge es una red interna de Docker aislada de la red host. Puede acceder a los contenedores solo a través de los puertos que abre cuando el contenedor comienza con el comando
-p . Puede crear cualquier cantidad de redes, como
bridge .
Ninguno es una red separada para un contenedor específico.
Host es la red de host. Cuando selecciona esta red, su contenedor es completamente accesible a través del host: el comando
-p simplemente no funciona aquí, y si implementó el contenedor en esta red, entonces no necesita especificar un puerto externo: el contenedor es accesible por su puerto interno. Por ejemplo, si Dockerfile EXPOSE está configurado en 8090, es a través de este puerto que la aplicación estará disponible.

Como necesitamos tener acceso a la base de datos del servidor, utilizaremos el último método y diseñaremos los contenedores en la red del servidor remoto.
Esto se hace de manera muy simple, eliminamos la mención de puertos del comando de inicio del contenedor y especificamos la red host:
docker run --net=host xpendence/rebounder-chain-frontend:0.0.8
Conexión a la base que indiqué
localhost:3306
La conexión de la parte frontal a la posterior tenía que especificarse por completo, externa:
http://<__:__>
Si reenvía el puerto interno al puerto externo, que suele ser el caso de los servidores remotos, debe especificar el puerto interno para la base de datos y el puerto externo para el contenedor.
Si desea experimentar con conexiones, puede descargar y crear un proyecto que escribí especialmente para probar la conexión entre contenedores. Simplemente ingrese la dirección deseada, presione Enviar y en modo de depuración, vea lo que voló.
El proyecto yace
aquí .
Conclusión
Hay muchas formas de crear y ejecutar una imagen de Docker. Para aquellos interesados, le aconsejo que aprenda docker-compose. Aquí hemos examinado solo una de las formas de trabajar con Docker. Por supuesto, este enfoque al principio no parece tan simple. Pero aquí hay un ejemplo: durante la redacción de una publicación, tuve conexiones salientes en un servidor remoto. Y durante el proceso de depuración, tuve que cambiar la configuración de conexión de la base de datos varias veces. Todo el ensamblaje y la implementación encajan en mi conjunto de 4 líneas, luego de ingresar, vi el resultado en un servidor remoto. En modo de programación extrema, Docker es indispensable.
Como prometí, publico las fuentes de la aplicación:
backendfrontend