Привет, друзья!
В этой статье я продолжаю (и заканчиваю) делиться с вами заметками о Docker.
Заметки состоят из 4 частей: 2 теоретических и 2 практических.
Если быть более конкретным:
В этой заключительной части мы "контейнеризуем" наше приложение.
Репозиторий с кодом приложения.
Если вам это интересно, прошу под кат.
Предполагается, что вы хотя бы вкратце ознакомились с содержанием предыдущих частей.
Клонировать и запустить приложение, с которым мы будем работать, можно следующим образом:
git clone https://github.com/harryheman/docker-test.git
cd docker-test
cd client
yarn
# or
npm i
cd ../admin
yarn
cd ../api
yarn
cd ..
yarn
yarn dev
# or
npm run dev
Если вы используете npm, команды для запуска серверов для разработки в файле package.json должны выглядеть так:
"scripts": {
"dev:client": "npm run start --prefix services/client",
"dev:admin": "npm run dev --prefix services/admin",
"dev:api": "npm run dev --prefix services/api",
"dev": "concurrently \"npm run dev:client\" \"npm run dev:admin\" \"npm run dev:api\""
}
Начнем с определения Dockerfile для сервисов нашего приложения.
В директории client создаем файл Dockerfile следующего содержания:
# дефолтная версия `Node.js`
ARG NODE_VERSION=16.13.1
# используемый образ
FROM node:$NODE_VERSION
# рабочая директория
WORKDIR /client
# копируем указанные файлы в рабочую директорию
COPY package.json yarn.lock ./
# устанавливаем зависимости
RUN yarn
# копируем остальные файлы
COPY . .
# выполняем сборку приложения
RUN yarn build
Обратите внимание: на данном этапе вместо сборки (RUN yarn build) мы могли бы выполнять команду start для запуска сервера для разработки: CMD ["yarn", "start"], но если мы так сделаем, то впоследствии нам придется создавать отдельный Dockerfile для продакшна. Проще сразу определить производственную версию Dockerfile, а команду start запускать из docker-compose.yml.
Создаем практический идентичный Dockerfile в директории admin:
ARG NODE_VERSION=16.13.1
FROM node:$NODE_VERSION as build
WORKDIR /admin
COPY package.json yarn.lock ./
RUN yarn
COPY . .
RUN yarn build
Обратите внимание: сборка клиента будет находится в директории client/build, а сборка админки — в директории admin/dist. В файле api/index.js можно найти такие строки:
if (process.env.ENV === 'production') {
const clientBuildPath = join(__dirname, 'client', 'build')
const adminDistPath = join(__dirname, 'admin', 'dist')
app.use(express.static(clientBuildPath))
app.use(express.static(adminDistPath))
app.use('/admin', (req, res) => {
res.sendFile(join(adminDistPath, decodeURIComponent(req.url)))
})
}
Эти строки говорят нам о том, что при запуске сервера в производственном режиме (process.env.ENV === 'production'), он будет обслуживать статические файлы из названных выше директорий: клиент будет доступен по маршруту (роуту) /, а админка — по роуту /admin. Мы вернемся к этому позже.
Создаем похожий Dockerfile в директории api:
ARG NODE_VERSION=16.13.1
FROM node:$NODE_VERSION
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn
COPY . .
# выставляем порт
EXPOSE 5000
# запускаем сервер в производственном режиме
CMD ["yarn", "start"]
Обратите внимание: инструкции EXPOSE 5000 и CMD ["yarn", "start"] на данном этапе можно опустить, но они потребуются нам в продакшне. На самом деле, нам потребуется кое-что еще, но позвольте пока сохранить интригу.
Также обратите внимание, что я внес парочку изменений в проект:
Хорошей практикой считается исключение файлов из образа с помощью .dockerignore:
node_modules
yarn-error.log
# mac
.DS_Store
Такой файл нужно создать в каждом сервисе.
После создания Dockerfile для каждого сервиса мы готовы к "контейнеризации" приложения.
Создаем в корневой директории файл docker-compose.dev.yml следующего содержания:
# версия `compose`
version: '3.9'
# сервисы
services:
# БД
postgres:
# файл, содержащий переменные среды окружения
env_file: .env
# название контейнера
container_name: ${APP_NAME}_postgres
# используемый образ
image: postgres:${POSTGRES_VERSION}
# именованный том для хранения данных
volumes:
- data_postgres:/var/lib/postgresql/data
# порт для доступа к БД
ports:
- 5432:5432
# политика перезапуска контейнера
restart: on-failure
client:
env_file: .env
container_name: ${APP_NAME}_client
image: node:${NODE_VERSION}
# рабочая директория
working_dir: /app
# анонимный том
# `rw` означает `read/write` - чтение/запись
volumes:
- ./services/client:/app:rw
# сервис, от которого зависит работоспособность данного сервиса
depends_on:
- api
ports:
- 3000:3000
restart: on-failure
# команда для запуска сервера для разработки
command: bash -c "yarn start"
admin:
env_file: .env
container_name: ${APP_NAME}_admin
image: node:${NODE_VERSION}
working_dir: /app
volumes:
- ./services/admin:/app:rw
depends_on:
- api
ports:
- 4000:4000
restart: on-failure
command: bash -c "yarn dev"
api:
env_file: .env
container_name: ${APP_NAME}_api
# ссылка на `Dockerfile`, на основе которого выполняется сборка
build: services/api
ports:
- 5000:5000
depends_on:
- postgres
restart: on-failure
# перезапись команды `yarn start`, определенной в `Dockerfile`
command: bash -c "yarn dev"
# тома
volumes:
data_postgres:
Определим в package.json несколько команд для управления compose:
"dev:compose:up": "docker compose -f docker-compose.dev.yml up -d",
"dev:compose:stop": "docker compose -f docker-compose.dev.yml stop",
"dev:compose:rm": "docker compose -f docker-compose.dev.yml rm",
"compose:up": "docker compose up -d",
"compose:stop": "docker compose stop",
"compose:rm": "docker compose rm"
Команда compose:up поднимает, команда compose:stop — останавливает, а команда compose:rm — удаляет сервис. Префикс dev: означает что поднимается/останавливается/удаляется сервис для разработки. В свою очередь, отсутствие данного префикса означает управление производственным сервисом (по умолчанию compose использует файл docker-compose.yml, которым мы займемся позже).
Еще несколько команд, которые могут пригодится при работе с compose при отладке приложения:
# список запущенных контейнеров
docker ps
# список запущенных сервисов
docker compose ls
# список образов
docker images
# удаление образа
# [image-name] - название образа
docker image rm [image-name]
# например
docker image rm docker-test_api
# список томов
docker volume ls
# удаление тома
# [volume-name] - название тома
docker volume rm [volume-name]
# например
docker volume rm postgres_data
# очистка системы (тома не удаляются)
docker system prune -a
Поднимаем сервис в режиме для разработки с помощью команды yarn dev:compose:up или npm run dev:compose:up:
После создания контейнеров сервисам потребуется какое-то время на запуск, после чего они будут доступны по следующим адресам:
По сути, команда dev:compose:up делает тоже самое, что и команда dev + скрипт из файла db.
Чем производственный сервис будет отличаться от сервиса для разработки? Предположим, что мы хотим, чтобы всю статику приложения обслуживал сервер, поэтому нам требуется какой-то способ передать api сборки клиента и админки. Существует несколько способов это сделать. Одним из самых простых и удобных является использование Docker Hub.
Переходим по ссылке и создаем аккаунт.
Переходим в директорию client и создаем образ с тегом:
cd client
# [username] - ваш логин для входа в dockerhub
# тег образа обязательно должен начинаться с вашего логина
docker build . -t [username]/docker-test_client
# мой логин - aio350
docker build . -t aio350/docker-test_client
Авторизуемся в dockerhub и отправляем образ в свой реестр:
docker login
docker push aio350/docker-test_client
Делаем тоже самое для админки:
cd admin
docker build . -t aio350/docker-test_admin
docker push aio350/docker-test_admin
После этого в своем реестре dockerhub мы увидим следующую картину:
Немного отредактируем файл api/Dockerfile:
ARG NODE_VERSION=16.13.1
# копируем образ клиента из `dockerhub`
# `AS` позволяет ссылаться на этот слой в других инструкциях
FROM aio350/docker-test_client AS client
# образ админки
FROM aio350/docker-test_admin AS admin
FROM node:$NODE_VERSION
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn
COPY . .
# копируем сборку клиента
COPY --from=client /client/build /app/client/build
# копируем сборку админки
COPY --from=admin /admin/dist /app/admin/dist
EXPOSE 5000
CMD ["yarn", "start"]
Создаем в корневой директории проекта файл docker-compose.yml следующего содержания:
version: '3.9'
services:
postgres:
env_file: .env
container_name: ${APP_NAME}_postgres
image: postgres:${POSTGRES_VERSION}
volumes:
- data_postgres:/var/lib/postgresql/data
ports:
- 5432:5432
restart: on-failure
# статика нашего приложения обслуживается сервером
# поэтому нам не нужно поднимать сервисы `client` и `admin`
api:
env_file: .env
# перезаписываем переменную `ENV`, определенную в файле `.env`
environment:
- ENV=production
container_name: ${APP_NAME}_api
build: services/api
depends_on:
- postgres
ports:
- 5000:5000
restart: on-failure
# выполняется команда `yarn start`, определенная в `Dockerfile`
volumes:
data_postgres:
Удаляем сервис для разработки, удаляем образ docker-test_api, удаляем том docker-test_data_postgres и поднимаем производственный сервис:
yarn dev:compose:stop
yarn dev:compose:rm
# для чистоты эксперимента
docker image rm docker-test_api
docker volume rm docker-test_data_postgres
yarn compose:up
Теперь наш сервис состоит всего из 2 контейнеров.
Клиент доступен по адресу: localhost:5000, а админка — по адресу localhost:5000/admin.
Приложение работает, как ожидается.
На этом "контейнеризацию" нашего приложения можно считать завершенной.
Что касается настройки CI/CD, такого как GitLab CI/CD или GitHub Actions, то, пожалуй, это тема для отдельной статьи.
Пожалуй, это все, что я хотел рассказать вам о Docker.
Благодарю за внимание и happy coding!
habr.com
В этой статье я продолжаю (и заканчиваю) делиться с вами заметками о Docker.
Заметки состоят из 4 частей: 2 теоретических и 2 практических.
Если быть более конкретным:
- первая часть посвящена Docker, Docker CLI и Dockerfile;
- во второй части рассказывается о Docker Compose;
- в третьей части мы разрабатываем приложение, состоящее из трех сервисов: клиента на React, админки на Vue и сервера на Express, и базы данных PostgreSQL, взаимодействие с которой осуществляется с помощью Prisma.
В этой заключительной части мы "контейнеризуем" наше приложение.
Репозиторий с кодом приложения.
Если вам это интересно, прошу под кат.
Предполагается, что вы хотя бы вкратце ознакомились с содержанием предыдущих частей.
Клонировать и запустить приложение, с которым мы будем работать, можно следующим образом:
git clone https://github.com/harryheman/docker-test.git
cd docker-test
cd client
yarn
# or
npm i
cd ../admin
yarn
cd ../api
yarn
cd ..
yarn
yarn dev
# or
npm run dev
Если вы используете npm, команды для запуска серверов для разработки в файле package.json должны выглядеть так:
"scripts": {
"dev:client": "npm run start --prefix services/client",
"dev:admin": "npm run dev --prefix services/admin",
"dev:api": "npm run dev --prefix services/api",
"dev": "concurrently \"npm run dev:client\" \"npm run dev:admin\" \"npm run dev:api\""
}
Dockerfile
Начнем с определения Dockerfile для сервисов нашего приложения.
В директории client создаем файл Dockerfile следующего содержания:
# дефолтная версия `Node.js`
ARG NODE_VERSION=16.13.1
# используемый образ
FROM node:$NODE_VERSION
# рабочая директория
WORKDIR /client
# копируем указанные файлы в рабочую директорию
COPY package.json yarn.lock ./
# устанавливаем зависимости
RUN yarn
# копируем остальные файлы
COPY . .
# выполняем сборку приложения
RUN yarn build
Обратите внимание: на данном этапе вместо сборки (RUN yarn build) мы могли бы выполнять команду start для запуска сервера для разработки: CMD ["yarn", "start"], но если мы так сделаем, то впоследствии нам придется создавать отдельный Dockerfile для продакшна. Проще сразу определить производственную версию Dockerfile, а команду start запускать из docker-compose.yml.
Создаем практический идентичный Dockerfile в директории admin:
ARG NODE_VERSION=16.13.1
FROM node:$NODE_VERSION as build
WORKDIR /admin
COPY package.json yarn.lock ./
RUN yarn
COPY . .
RUN yarn build
Обратите внимание: сборка клиента будет находится в директории client/build, а сборка админки — в директории admin/dist. В файле api/index.js можно найти такие строки:
if (process.env.ENV === 'production') {
const clientBuildPath = join(__dirname, 'client', 'build')
const adminDistPath = join(__dirname, 'admin', 'dist')
app.use(express.static(clientBuildPath))
app.use(express.static(adminDistPath))
app.use('/admin', (req, res) => {
res.sendFile(join(adminDistPath, decodeURIComponent(req.url)))
})
}
Эти строки говорят нам о том, что при запуске сервера в производственном режиме (process.env.ENV === 'production'), он будет обслуживать статические файлы из названных выше директорий: клиент будет доступен по маршруту (роуту) /, а админка — по роуту /admin. Мы вернемся к этому позже.
Создаем похожий Dockerfile в директории api:
ARG NODE_VERSION=16.13.1
FROM node:$NODE_VERSION
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn
COPY . .
# выставляем порт
EXPOSE 5000
# запускаем сервер в производственном режиме
CMD ["yarn", "start"]
Обратите внимание: инструкции EXPOSE 5000 и CMD ["yarn", "start"] на данном этапе можно опустить, но они потребуются нам в продакшне. На самом деле, нам потребуется кое-что еще, но позвольте пока сохранить интригу.
Также обратите внимание, что я внес парочку изменений в проект:
- Содержание файла .env, находящегося корневой директории проекта:
# добавил название приложения
APP_NAME=my-app
# уточнил версию `Node.js`
NODE_VERSION=16.13.1
POSTGRES_VERSION=14
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=mydb
# перенес сюда путь к БД из файла `api/.env`
# обратите внимание, что вместо `localhost` после символа `@` мы указываем название контейнера -
`postgres`
DATABASE_URL=postgresql://postgresostgres@postgres:5432/mydb?schema=public
ENV=development
- Команда для запуска сервера для разработки (файл api/package.json, раздел scripts):
"dev": "prisma migrate dev && prisma db seed && nodemon",
Хорошей практикой считается исключение файлов из образа с помощью .dockerignore:
node_modules
yarn-error.log
# mac
.DS_Store
Такой файл нужно создать в каждом сервисе.
После создания Dockerfile для каждого сервиса мы готовы к "контейнеризации" приложения.
Docker Compose
Создаем в корневой директории файл docker-compose.dev.yml следующего содержания:
# версия `compose`
version: '3.9'
# сервисы
services:
# БД
postgres:
# файл, содержащий переменные среды окружения
env_file: .env
# название контейнера
container_name: ${APP_NAME}_postgres
# используемый образ
image: postgres:${POSTGRES_VERSION}
# именованный том для хранения данных
volumes:
- data_postgres:/var/lib/postgresql/data
# порт для доступа к БД
ports:
- 5432:5432
# политика перезапуска контейнера
restart: on-failure
client:
env_file: .env
container_name: ${APP_NAME}_client
image: node:${NODE_VERSION}
# рабочая директория
working_dir: /app
# анонимный том
# `rw` означает `read/write` - чтение/запись
volumes:
- ./services/client:/app:rw
# сервис, от которого зависит работоспособность данного сервиса
depends_on:
- api
ports:
- 3000:3000
restart: on-failure
# команда для запуска сервера для разработки
command: bash -c "yarn start"
admin:
env_file: .env
container_name: ${APP_NAME}_admin
image: node:${NODE_VERSION}
working_dir: /app
volumes:
- ./services/admin:/app:rw
depends_on:
- api
ports:
- 4000:4000
restart: on-failure
command: bash -c "yarn dev"
api:
env_file: .env
container_name: ${APP_NAME}_api
# ссылка на `Dockerfile`, на основе которого выполняется сборка
build: services/api
ports:
- 5000:5000
depends_on:
- postgres
restart: on-failure
# перезапись команды `yarn start`, определенной в `Dockerfile`
command: bash -c "yarn dev"
# тома
volumes:
data_postgres:
Определим в package.json несколько команд для управления compose:
"dev:compose:up": "docker compose -f docker-compose.dev.yml up -d",
"dev:compose:stop": "docker compose -f docker-compose.dev.yml stop",
"dev:compose:rm": "docker compose -f docker-compose.dev.yml rm",
"compose:up": "docker compose up -d",
"compose:stop": "docker compose stop",
"compose:rm": "docker compose rm"
Команда compose:up поднимает, команда compose:stop — останавливает, а команда compose:rm — удаляет сервис. Префикс dev: означает что поднимается/останавливается/удаляется сервис для разработки. В свою очередь, отсутствие данного префикса означает управление производственным сервисом (по умолчанию compose использует файл docker-compose.yml, которым мы займемся позже).
Еще несколько команд, которые могут пригодится при работе с compose при отладке приложения:
# список запущенных контейнеров
docker ps
# список запущенных сервисов
docker compose ls
# список образов
docker images
# удаление образа
# [image-name] - название образа
docker image rm [image-name]
# например
docker image rm docker-test_api
# список томов
docker volume ls
# удаление тома
# [volume-name] - название тома
docker volume rm [volume-name]
# например
docker volume rm postgres_data
# очистка системы (тома не удаляются)
docker system prune -a
Поднимаем сервис в режиме для разработки с помощью команды yarn dev:compose:up или npm run dev:compose:up:


После создания контейнеров сервисам потребуется какое-то время на запуск, после чего они будут доступны по следующим адресам:
- клиент: localhost:3000;
- админка: localhost:4000;
- сервер: localhost:5000 (нет прямого доступа; доступен для клиента и админки);
- БД: postgres:5432 (нет прямого доступа; доступен только для сервера).
По сути, команда dev:compose:up делает тоже самое, что и команда dev + скрипт из файла db.
Чем производственный сервис будет отличаться от сервиса для разработки? Предположим, что мы хотим, чтобы всю статику приложения обслуживал сервер, поэтому нам требуется какой-то способ передать api сборки клиента и админки. Существует несколько способов это сделать. Одним из самых простых и удобных является использование Docker Hub.
Переходим по ссылке и создаем аккаунт.
Переходим в директорию client и создаем образ с тегом:
cd client
# [username] - ваш логин для входа в dockerhub
# тег образа обязательно должен начинаться с вашего логина
docker build . -t [username]/docker-test_client
# мой логин - aio350
docker build . -t aio350/docker-test_client
Авторизуемся в dockerhub и отправляем образ в свой реестр:
docker login
docker push aio350/docker-test_client
Делаем тоже самое для админки:
cd admin
docker build . -t aio350/docker-test_admin
docker push aio350/docker-test_admin
После этого в своем реестре dockerhub мы увидим следующую картину:

Немного отредактируем файл api/Dockerfile:
ARG NODE_VERSION=16.13.1
# копируем образ клиента из `dockerhub`
# `AS` позволяет ссылаться на этот слой в других инструкциях
FROM aio350/docker-test_client AS client
# образ админки
FROM aio350/docker-test_admin AS admin
FROM node:$NODE_VERSION
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn
COPY . .
# копируем сборку клиента
COPY --from=client /client/build /app/client/build
# копируем сборку админки
COPY --from=admin /admin/dist /app/admin/dist
EXPOSE 5000
CMD ["yarn", "start"]
Создаем в корневой директории проекта файл docker-compose.yml следующего содержания:
version: '3.9'
services:
postgres:
env_file: .env
container_name: ${APP_NAME}_postgres
image: postgres:${POSTGRES_VERSION}
volumes:
- data_postgres:/var/lib/postgresql/data
ports:
- 5432:5432
restart: on-failure
# статика нашего приложения обслуживается сервером
# поэтому нам не нужно поднимать сервисы `client` и `admin`
api:
env_file: .env
# перезаписываем переменную `ENV`, определенную в файле `.env`
environment:
- ENV=production
container_name: ${APP_NAME}_api
build: services/api
depends_on:
- postgres
ports:
- 5000:5000
restart: on-failure
# выполняется команда `yarn start`, определенная в `Dockerfile`
volumes:
data_postgres:
Удаляем сервис для разработки, удаляем образ docker-test_api, удаляем том docker-test_data_postgres и поднимаем производственный сервис:
yarn dev:compose:stop
yarn dev:compose:rm
# для чистоты эксперимента
docker image rm docker-test_api
docker volume rm docker-test_data_postgres
yarn compose:up

Теперь наш сервис состоит всего из 2 контейнеров.
Клиент доступен по адресу: localhost:5000, а админка — по адресу localhost:5000/admin.


Приложение работает, как ожидается.
На этом "контейнеризацию" нашего приложения можно считать завершенной.
Что касается настройки CI/CD, такого как GitLab CI/CD или GitHub Actions, то, пожалуй, это тема для отдельной статьи.
Пожалуй, это все, что я хотел рассказать вам о Docker.
Благодарю за внимание и happy coding!

Docker: заметки веб-разработчика. Итерация четвертая
Привет, друзья! В этой статье я продолжаю (и заканчиваю) делиться с вами заметками о Docker . Заметки состоят из 4 частей: 2 теоретических и 2 практических. Если быть более конкретным: первая часть...
