Решил описать свой подход построения окружения на Typescript с Nest на бекенде, Nuxt (SPA) на фронтенде. Все заворачивается в один docker‑образ и запускается как standalone приложение c nginx, healthcheck»ами, тестами и ш…широкой сферой применения.
Делал это в качестве фундамента для будущих проектов или с целью изучения Nest, Nuxt 3 с composable функциями. Можно использовать это как инструкцию к настройке подобной архитектуры, можно взять за основу код с github.
Схема архитектуры
└── application/
├── backend/
│ └── NEST приложение
├── frontend/
│ └── NUXT приложение
├── docker/
│ └── nginx/
│ └── conf.conf
├── .dockerignore
├── .gitignore
├── docker-compose.yml
├── Dockerfile
└── readme.md
Таким образом может быть 2 типа запроса.
Статический:
И запрос к API:
$ npm i -g @nestjs/cli
$ nest new backend
Дальше необходимо сделать некоторые доработки. Первым делом в main.ts прописываем порт по умолчанию на 3001, добавляем префикс /api. Таким образом main.ts обретает следующий вид:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = process.env.APP_PORT || 3001;
app.setGlobalPrefix('api');
app.enableCors();
await app.listen(port);
}
bootstrap();
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ServeStaticModule } from '@nestjs/serve-static';
import * as path from 'path';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: path.join(__dirname, '..', 'static'),
serveRoot: '/',
exclude: ['/api*'],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Обратите внимание на блок exclude: ['/api*']. Это нужно для того, чтобы статика раздавалась на всех ссылках, кроме /api — при запуске проекта по пути /api будет размещаться само nest приложение.
В саму папку static размещаем .gitignore с двумя строчками:
*
!index.html
И index.html, который будет использоваться только при разработке и при сборке конечного docker-образа в эту папку будет складываться html/js/css интерфейса.
export async function createTestingApp() {
return (
await Test.createTestingModule({
imports: [AppModule],
}).compile()
)
.createNestApplication()
.setGlobalPrefix('api');
}
Дальше я уже, в тестах, использую эту функцию вместо штатной инициализации приложения:
import { createTestingApp } from './utils/create-testing-app';
// .....
beforeEach(async () => {
app = await createTestingApp();
await app.init();
});
yarn create nuxt-app frontend
Важный момент: я не буду использовать Nuxt с SSR, т.к. у меня планируется чисто SPA подход (когда браузер загружает целиком весь код к себе и дальше уже рендерит интерфейс).
Да, SSR классно и здорово, но считаю его уместным в проектах с необходимостью поддерживать SEO или если необходимо часть логики отображения скрыть от пользователя (чтобы не показывать какие‑то переменные окружения).
В любом случае, при необходимости, данный стартовый набор можно «переобуть» на работу с SSR. Что бы выключить SSR режим надо в nuxt.config.ts указать ssr: false
Я же остановился, пока что, на подходе через options и постепенно внедряю compose функции в небольших проектах. В текущем наборе я выбрал Composition подход, т.к. тут функционала почти нет и заодно можно попробовать.
<template>
<div>
<NuxtPage />
</div>
</template>
После чего каждый файл в папке pages/ будет открываться по одноименной ссылке в браузере. Подробнее можно прочитать здесь.
Сейчас я сделаю по‑современному через создание своей composable функции, расширяющей useFetch. В Nuxt composables создаются автоматически, создав файл в папке composables.
// frontend/composables/api.ts
import { UseFetchOptions } from '#app';
import { NitroFetchRequest } from 'nitropack';
import { KeyOfRes } from 'nuxt/dist/app/composables/asyncData';
export function useApiRequest<T>(
request: NitroFetchRequest,
opts?:
| UseFetchOptions<T extends void ? unknown : T,
(res: T extends void ? unknown : T) => T extends void ? unknown : T,
KeyOfRes<(res: T extends void ? unknown : T) => T extends void ? unknown : T>>
| undefined
) {
const config = useRuntimeConfig();
return useFetch(request, {baseURL: config.public.baseURL, ...opts});
}
Чтобы конструкция config.public.baseURL работала, необходимо расширить nuxt.config.ts следующим образом:
export default defineNuxtConfig({
ssr: false,
runtimeConfig: {
public: {
baseURL: process.env.API_URL || 'http://localhost:3001/',
},
},
})
И теперь, по умолчанию, baseURL будет равен http://localhost:3001/, чтобы, при разработке, стучаться в отдельно запущенный Nest. При сборке буду менять его на /api.
<template>
<div>
<template v-if="pending">
Loading
</template>
<template v-else>
<template v-if="data">
Api result: {{ data }}
</template>
<template v-else-if="error">
Api ERROR: {{ error }}
</template>
<button @click="refresh()">refresh</button>
</template>
</div>
</template>
<script setup>
import { useApiRequest } from '../composables/api'
const { data, pending, error, refresh } = useApiRequest('/api/test')
</script>
Основная идея сборки состоит из следущих этапов:
FROM node:16-alpine as base-builder
WORKDIR /app
Для начала нужно собрать frontend — подтянуть зависимости, собрать html, js, css.
FROM base-builder as build_fe
WORKDIR /app
COPY ./frontend/package.json ./frontend/yarn.lock* ./
RUN yarn install
ADD ./frontend ./
RUN yarn generate
По итогу в этом промежуточном образе у нас будет собранный frontend в папке /app/dist
Далее собираем backend
FROM base-builder as build_be
WORKDIR /app
COPY ./backend/package.json ./backend/yarn.lock* ./
RUN yarn install
ADD ./backend ./
RUN yarn build
И получаем промежуточный образ только с backend. Теперь осталось собрать воедино в следующий промежуточный образ, который будет на 3001 порту слушать все запросы:
FROM node:16-alpine as finalNode
WORKDIR /app
COPY --from=build_be /app /app
COPY --from=build_fe /app/dist /app/static
CMD yarn start
Я до конца не определился в необходимости этого этапа и, честно говоря, его можно и не делать. У нас, в итоге, получается backend, который умеет также отдавать статику приложения — то есть полностью самостоятельно рабочий docker‑образ с приложением, который может работать без nginx. Но именно в рамках текущей статьи эта возможность не используется
Теперь осталось собрать ту часть, которая будет с nginx:
FROM nginx:alpine as finalNginx
WORKDIR /usr/share/nginx/html
RUN rm -rf ./*
COPY --from=finalNode /app/static .
COPY ./docker/nginx/conf.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
Также надо не забыть положить файл конфигурации nginx по указанном пути:
# docker/nginx/conf.conf
server {
listen 80 default_server;
root /usr/share/nginx/html;
client_max_body_size 20M;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://node:3001;
}
}
Теперь мы, в рамках одного Dockerfile получили полную сборку всего, что нужно для работы приложения.
Итоговый Dockerfile можно посмотреть в github репозитории.
version: "3.8"
services:
# тут будут сервисы
networks:
reverse-proxy:
external:
name: reverse-proxy
back:
driver: bridge
Также я добавил сеть back — это изолированная сеть, через которую между собой будут общаться nginx и backend.
Теперь опишем как мы будем запускать наш образ с той частью, которая отвечает за backend:
node:
build:
context: .
target: finalNode
networks:
- back
expose:
- 3001
restart: always
environment:
- APP_PORT=3001
healthcheck:
test: wget --no-verbose --tries=1 --spider <http://localhost:3001> || exit 1
timeout: 3s
interval: 3s
retries: 10
По порядку о каждом параметре:
nginx:
build:
context: .
target: finalNginx
networks:
- reverse-proxy
- back
expose:
- 80
restart: always
depends_on:
node:
condition: service_healthy
environment:
- VIRTUAL_HOST=${DOMAIN}
- VIRTUAL_PORT=80
- LETSENCRYPT_HOST=${DOMAIN}
- LETSENCRYPT_EMAIL=test@test.ru
Подробнее:
#.dockerignore
.idea
.git
**/.nuxt
**/dist
**/.output
**/node_modules
**/.env
Также создал .env.example в качестве файла-примера:
DOMAIN=domain.ru
Далее необходимо на сервере запустить nginx-proxy и лучше это делать в отдельном месте на том‑же сервере (инструкция здесь, но если нужно, то напишите в комментариях и дополню эту инструкцию здесь).
DOMAIN=domain.ru && echo DOMAIN=$DOMAIN > .env && docker-compose up -d --build
Важно: заменить domain.ru на свой домен, который уже направлен на сервер, где мы запускаем сервис.
git fetch && git reset --hard origin/master && docker-compose up -d --build
И проект обновится и запустит обновленную версию на домене.
Если подобный формат полезен и интересен для дальнейшего описания, то в следующих статьях опишу подобный стартовый набор с базой данных (Postgres) и авторизацией (JWT). Также есть мысль описать процесс подготовки и настройки ansible для подобных проектов.
habr.com

Делал это в качестве фундамента для будущих проектов или с целью изучения Nest, Nuxt 3 с composable функциями. Можно использовать это как инструкцию к настройке подобной архитектуры, можно взять за основу код с github.
Архитектура проекта
Шаблон приложения поставляется в виде одного docker‑образа, в котором установлен nest+nginx и собраны backend и frontend.
Файловая структура
Для начала опишу из как выглядит архитектура проекта.└── application/
├── backend/
│ └── NEST приложение
├── frontend/
│ └── NUXT приложение
├── docker/
│ └── nginx/
│ └── conf.conf
├── .dockerignore
├── .gitignore
├── docker-compose.yml
├── Dockerfile
└── readme.md
- backend — стандартное nest приложение с добавленным serve-static модулем;
- frontend — стандартное nuxt приложение с добавленным и настроенной связью с backend;
- docker — папка с конфигами, которые пойдут в docker образ (в текущей версии только nginx);
- Dockerfile — указания по сборке докер-образа;
- docker-compose.yml — файл для запуска проекта.
В этом шаблоне нет базы данных и каких‑либо других сторонних зависимостей, чтобы не ограничивать набор компонентов для дальнейшей разработки.
Процесс обработки запросов
В качестве сервера, принимающего запросы используется Nginx. Он раздает статику собранного frontend приложения и перенаправляет запрос на бекенд, если URL запроса начинается на /apiТаким образом может быть 2 типа запроса.
Статический:

И запрос к API:

Подготовка Backend сервиса
За основу взят стартовый набор nest:$ npm i -g @nestjs/cli
$ nest new backend
Дальше необходимо сделать некоторые доработки. Первым делом в main.ts прописываем порт по умолчанию на 3001, добавляем префикс /api. Таким образом main.ts обретает следующий вид:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = process.env.APP_PORT || 3001;
app.setGlobalPrefix('api');
app.enableCors();
await app.listen(port);
}
bootstrap();
Настройка static директории
В папку static будет переноситься статичный html/js/css бандл с nuxt приложением и потом раздаваться как статичный сайт при запуске проекта без nginx.Для того, чтобы nest раздавал статику достаточно подключить модуль serve‑static внутрь AppModuleДа, при прочих равных, стоит запускать проект с nginx и для этого не нужно переносить в папку static ничего.
Но на то это и бойлер, что я заранее не знаю как он будет и где запускаться. Может быть, в каких то ситуациях, при малых нагрузках, будет достаточно запуска чистого Nest.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ServeStaticModule } from '@nestjs/serve-static';
import * as path from 'path';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: path.join(__dirname, '..', 'static'),
serveRoot: '/',
exclude: ['/api*'],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Обратите внимание на блок exclude: ['/api*']. Это нужно для того, чтобы статика раздавалась на всех ссылках, кроме /api — при запуске проекта по пути /api будет размещаться само nest приложение.
В саму папку static размещаем .gitignore с двумя строчками:
*
!index.html
И index.html, который будет использоваться только при разработке и при сборке конечного docker-образа в эту папку будет складываться html/js/css интерфейса.
Небольшое отступление по поводу префикса к api
В nest можно реализовать префикс /api двумя способами:- в каждом контроллере приписывать /api в @Controller('/api/controller-route')
- прописать на уровне nest приложения глобальный префикс
- Прописать в main.ts строчку app.setGlobalPrefix('api');
- Поправить e2e тест, чтобы в нем тоже создавалось приложение с префиксом и поправить сами тесты.
export async function createTestingApp() {
return (
await Test.createTestingModule({
imports: [AppModule],
}).compile()
)
.createNestApplication()
.setGlobalPrefix('api');
}
Дальше я уже, в тестах, использую эту функцию вместо штатной инициализации приложения:
import { createTestingApp } from './utils/create-testing-app';
// .....
beforeEach(async () => {
app = await createTestingApp();
await app.init();
});
Настройка тестового окружения
Я уже немного затронул тестовое окружение в предыдущем блоке, но по тестам я сделал еще небольшие изменения.- удалил стандартный spec файл у контроллера, т.к. сам предпочитаю e2e тесты и узкие тесты пишу в редких случаях;
- поменял формат jest.e2e.config.json на js, тк, зачастую, в проектах приходится добавлять динамические конфигурации и IDE js формат считывает сразу;
- поправил базовый тест с указанием /api в самих тестах.
Подготовка Frontend
В качестве фронта берется Nuxt 3 и ставится через официальную командуyarn create nuxt-app frontend
Важный момент: я не буду использовать Nuxt с SSR, т.к. у меня планируется чисто SPA подход (когда браузер загружает целиком весь код к себе и дальше уже рендерит интерфейс).
Да, SSR классно и здорово, но считаю его уместным в проектах с необходимостью поддерживать SEO или если необходимо часть логики отображения скрыть от пользователя (чтобы не показывать какие‑то переменные окружения).

В любом случае, при необходимости, данный стартовый набор можно «переобуть» на работу с SSR. Что бы выключить SSR режим надо в nuxt.config.ts указать ssr: false
Пара слов про Options и Composition
Если вы давно знакомы с Vue, то вы должны знать, что раньше все компоненты можно было делать vue компоненты только через Options подход (создавать объект с полями data, computed и тд). Сейчас появился подход через setup функцию и мне до конца не ясны прелести этого подхода.Я же остановился, пока что, на подходе через options и постепенно внедряю compose функции в небольших проектах. В текущем наборе я выбрал Composition подход, т.к. тут функционала почти нет и заодно можно попробовать.
Подключение NuxtPage
Изначально в App.vue не проставлен NuxtPage компонент и, следовательно, маршрутизация через файлы в pages работать не будет. Поэтому необходимо App.Vue привести к следующему виду:<template>
<div>
<NuxtPage />
</div>
</template>
После чего каждый файл в папке pages/ будет открываться по одноименной ссылке в браузере. Подробнее можно прочитать здесь.
Коннектор к API
Для реализации бизнес‑логики во Vue 3 разработчиками можно использовать Composable функции. Раньше я всегда делал подобные вещи в виде отдельного плагина с подстановкой хедера авторизации + указания baseUrl из env переменной.Сейчас я сделаю по‑современному через создание своей composable функции, расширяющей useFetch. В Nuxt composables создаются автоматически, создав файл в папке composables.
// frontend/composables/api.ts
import { UseFetchOptions } from '#app';
import { NitroFetchRequest } from 'nitropack';
import { KeyOfRes } from 'nuxt/dist/app/composables/asyncData';
export function useApiRequest<T>(
request: NitroFetchRequest,
opts?:
| UseFetchOptions<T extends void ? unknown : T,
(res: T extends void ? unknown : T) => T extends void ? unknown : T,
KeyOfRes<(res: T extends void ? unknown : T) => T extends void ? unknown : T>>
| undefined
) {
const config = useRuntimeConfig();
return useFetch(request, {baseURL: config.public.baseURL, ...opts});
}
Чтобы конструкция config.public.baseURL работала, необходимо расширить nuxt.config.ts следующим образом:
export default defineNuxtConfig({
ssr: false,
runtimeConfig: {
public: {
baseURL: process.env.API_URL || 'http://localhost:3001/',
},
},
})
И теперь, по умолчанию, baseURL будет равен http://localhost:3001/, чтобы, при разработке, стучаться в отдельно запущенный Nest. При сборке буду менять его на /api.
Пример использования API вызова
В качестве примера я оставил в компонент, который делает вызов в /api/test и проставляет в разметку все состояния запроса:<template>
<div>
<template v-if="pending">
Loading
</template>
<template v-else>
<template v-if="data">
Api result: {{ data }}
</template>
<template v-else-if="error">
Api ERROR: {{ error }}
</template>
<button @click="refresh()">refresh</button>
</template>
</div>
</template>
<script setup>
import { useApiRequest } from '../composables/api'
const { data, pending, error, refresh } = useApiRequest('/api/test')
</script>
Подготовка Docker-образа и docker-compose.yml
В своем личном блоге я писал и снимал про это видео, что небольшие проекты я разворачиваю достаточно топорным способом:- подготовить docker образ;
- подготовить docker-compose;
- развернуть на сервере nginx-proxy c acme-companion;
- запускать проект обычным docker-compose up -d и наслаждаться рабочим продуктом.
Основная идея сборки состоит из следущих этапов:
- Собрать frontend (html, js, css).
- Собрать backend.
- Подсунуть в backend файлы из frontend в папку static.
- Собрать nginx образ, который будет разбирать траффик на статику и логику.
Dockerfile с multi-stage build
В проекте я использую node 16 на базе образа alpine. Поэтому начинаем Dockerfile со строчекFROM node:16-alpine as base-builder
WORKDIR /app
Для начала нужно собрать frontend — подтянуть зависимости, собрать html, js, css.
FROM base-builder as build_fe
WORKDIR /app
COPY ./frontend/package.json ./frontend/yarn.lock* ./
RUN yarn install
ADD ./frontend ./
RUN yarn generate
По итогу в этом промежуточном образе у нас будет собранный frontend в папке /app/dist
Далее собираем backend
FROM base-builder as build_be
WORKDIR /app
COPY ./backend/package.json ./backend/yarn.lock* ./
RUN yarn install
ADD ./backend ./
RUN yarn build
И получаем промежуточный образ только с backend. Теперь осталось собрать воедино в следующий промежуточный образ, который будет на 3001 порту слушать все запросы:
FROM node:16-alpine as finalNode
WORKDIR /app
COPY --from=build_be /app /app
COPY --from=build_fe /app/dist /app/static
CMD yarn start
Я до конца не определился в необходимости этого этапа и, честно говоря, его можно и не делать. У нас, в итоге, получается backend, который умеет также отдавать статику приложения — то есть полностью самостоятельно рабочий docker‑образ с приложением, который может работать без nginx. Но именно в рамках текущей статьи эта возможность не используется
Теперь осталось собрать ту часть, которая будет с nginx:
FROM nginx:alpine as finalNginx
WORKDIR /usr/share/nginx/html
RUN rm -rf ./*
COPY --from=finalNode /app/static .
COPY ./docker/nginx/conf.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
Также надо не забыть положить файл конфигурации nginx по указанном пути:
# docker/nginx/conf.conf
server {
listen 80 default_server;
root /usr/share/nginx/html;
client_max_body_size 20M;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://node:3001;
}
}
Теперь мы, в рамках одного Dockerfile получили полную сборку всего, что нужно для работы приложения.
Итоговый Dockerfile можно посмотреть в github репозитории.
Подготовка docker-compose.yml
Как я писал выше, я запускаю подобные проекты на сервере, используя nginx-proxy. Так что, первым делом, в конце файла надо объявить сеть reverse-proxy, через которую будет идти подключение из внешнего мира к моему контейнеру с nginx.version: "3.8"
services:
# тут будут сервисы
networks:
reverse-proxy:
external:
name: reverse-proxy
back:
driver: bridge
Также я добавил сеть back — это изолированная сеть, через которую между собой будут общаться nginx и backend.
Теперь опишем как мы будем запускать наш образ с той частью, которая отвечает за backend:
node:
build:
context: .
target: finalNode
networks:
- back
expose:
- 3001
restart: always
environment:
- APP_PORT=3001
healthcheck:
test: wget --no-verbose --tries=1 --spider <http://localhost:3001> || exit 1
timeout: 3s
interval: 3s
retries: 10
По порядку о каждом параметре:
- build
- context — что будет являться текущей директорией при сборке Dockerfile;
- target — какую часть multi‑stage build нужно запускать в этом месте. В данном случае мы указываем, что собирать нужно все до finalNode.
- networks
- тут мы указываем только back, т.к. во внешний мир контейнер ходить не будет и нужен только доступ от nginx к этому контейнеру.
- expose
- этот пункт открывает доступ другим контейнерам в сети по перечисленным портам. В данном случае мы сообщаем, что в сети back контейнеры могут подключаться на 3001 порт.
- restart: always
- сообщаем, что этот контейнер надо перезапускать всегда. Даже после перезапуска сервера проект будет запущен;
- будет работать до тех пор пока не выключим его командой docker-compose down.
- environment
- передача переменных окружения в сам процесс node;
- в нашем случае только указываем порт, на котором мы хотим, чтобы backend был запущен.
- healthcheck
- прекрасный инструмент для контроля работоспособности контейнера;
- test — команда, от которой мы ожидаем exit-code = 0 (какие есть еще можно прочитать здесь);
- timeout — время, которое может выполняться команда. Если команда зависла на больший срок, то проверка считается не пройденой;
- interval — с какой частотой стоит выполнять команду, чтобы быть уверенным, что контейнер работает;
- retries — после скольких неудачных ответов сервер помечается «нерабочим».
nginx:
build:
context: .
target: finalNginx
networks:
- reverse-proxy
- back
expose:
- 80
restart: always
depends_on:
node:
condition: service_healthy
environment:
- VIRTUAL_HOST=${DOMAIN}
- VIRTUAL_PORT=80
- LETSENCRYPT_HOST=${DOMAIN}
- LETSENCRYPT_EMAIL=test@test.ru
Подробнее:
- build тоже самое, что в node сервисе, только указан другой target, т.к. нам нужно получить ту часть, которая связана с nginx;
- networks тут теперь 2 сети:
- reverse-proxy сеть, через которую будет доступ от контейнера nginx-proxy;
- back та сеть, в которой есть контейнер node чтобы можно было пересылать запросы ему.
- expose сообщаем всем в сетях, что в этот контейнер можно стучаться на 80 порт. Это нужно nginx-proxy для обработки запросов;
- restart аналогично сервису node;
- depends_on тут мы указываем от каких сервисов мы зависим:
- если это не указать, то nginx будет запускаться вместе с остальными и может получиться ситуация, в которой node еще не запущен, а nginx уже готов принимать запросы, что нехорошо;
- поэтому мы указываем, что зависит от node сервиса;
- зависимость можно считать удовлетворенной только когда сервис прошел свой healthcheck (как раз блок condition).
- environment
- тут мы указываем переменные окружения, которые нужны для работы nginx-proxy:
- VIRTUAL_HOST название домена доступа к приложению;
- VIRTUAL_PORT порт, на котором запущено приложение в контейнере;
- LETSENCRYPT_HOST тот же самый домен но уже для создания https сертификата;
- LETSENCRYPT_EMAIL электронная почта, куда писать о том, что скоро сертификат будет просрочен.
- тут используется внешняя переменная окружения ${DOMAIN} и она будет записаться из файла .env который будет лежать рядом с docker-compose.yml файлом (подробнее тут).
- тут мы указываем переменные окружения, которые нужны для работы nginx-proxy:
Дополнительные моменты в подготовке окружения
В корне проекта я создал файл .dockerignore чтобы, во время сборки, не перекачивать в контекст лишнего:#.dockerignore
.idea
.git
**/.nuxt
**/dist
**/.output
**/node_modules
**/.env
Также создал .env.example в качестве файла-примера:
DOMAIN=domain.ru
Запуск приложения
Подготовка сервера
Разумеется на сервере должен уже стоять Docker. Если нет, то установите его по официальной инструкции.Далее необходимо на сервере запустить nginx-proxy и лучше это делать в отдельном месте на том‑же сервере (инструкция здесь, но если нужно, то напишите в комментариях и дополню эту инструкцию здесь).
Запуск самого приложения
Запускается все это приложение очень простым образом:- Клонируем исходники.
- Прописываем DOMAIN в .env файл в корне проекта.
- Запускаем командой docker-compose up -d.
DOMAIN=domain.ru && echo DOMAIN=$DOMAIN > .env && docker-compose up -d --build
Важно: заменить domain.ru на свой домен, который уже направлен на сервер, где мы запускаем сервис.
Обновление версии приложения
Если нужно обновить исходники до последней версии, то можно выполнить следующую команду:git fetch && git reset --hard origin/master && docker-compose up -d --build
И проект обновится и запустит обновленную версию на домене.
Небольшое заключение
В конечном итоге получился вариант шаблона приложения на Nuxt + Nest который дальше можно расширять. Он крайне пуст — нет БД, авторизации и прочих базовых вещей. Разумеется в наших проектах есть разные шаблоны приложений, но я решил начать с описания самого базового варианта, который дальше можно развивать куда угодно.Если подобный формат полезен и интересен для дальнейшего описания, то в следующих статьях опишу подобный стартовый набор с базой данных (Postgres) и авторизацией (JWT). Также есть мысль описать процесс подготовки и настройки ansible для подобных проектов.

Подготовка шаблона приложения на Typescript с Nest, Nuxt 3 и Docker
Решил описать свой подход построения окружения на Typescript с Nest на бекенде, Nuxt (SPA) на фронтенде. Все заворачивается в один docker‑образ и запускается...
