{"id":329445,"date":"2022-02-11T15:01:16","date_gmt":"2022-02-11T15:01:16","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=329445"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=329445","title":{"rendered":"<span>Docker: \u0437\u0430\u043c\u0435\u0442\u043a\u0438 \u0432\u0435\u0431-\u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430. \u0418\u0442\u0435\u0440\u0430\u0446\u0438\u044f \u0442\u0440\u0435\u0442\u044c\u044f<\/span>"},"content":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body_version-1\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/webt\/b7\/dj\/ot\/b7djotv_rf_w4fh7wxkpeykxj6g.jpeg\" data-src=\"https:\/\/habrastorage.org\/webt\/b7\/dj\/ot\/b7djotv_rf_w4fh7wxkpeykxj6g.jpeg\" data-blurred=\"true\"\/>  <\/p>\n<p>  \u041f\u0440\u0438\u0432\u0435\u0442, \u0434\u0440\u0443\u0437\u044c\u044f! \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u044e \u0434\u0435\u043b\u0438\u0442\u044c\u0441\u044f \u0441 \u0432\u0430\u043c\u0438 \u0437\u0430\u043c\u0435\u0442\u043a\u0430\u043c\u0438 \u043e <a href=\"https:\/\/www.docker.com\/\"><code>Docker<\/code><\/a>.<\/p>\n<p>  <\/p>\n<p>\u0417\u0430\u043c\u0435\u0442\u043a\u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u0442 \u0438\u0437 4 \u0447\u0430\u0441\u0442\u0435\u0439: 2 \u0442\u0435\u043e\u0440\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0438 2 \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445. \u0415\u0441\u043b\u0438 \u0431\u044b\u0442\u044c \u0431\u043e\u043b\u0435\u0435 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u043c:<\/p>\n<p>  <\/p>\n<ul>\n<li><a href=\"https:\/\/habr.com\/ru\/company\/timeweb\/blog\/595687\/\">\u043f\u0435\u0440\u0432\u0430\u044f \u0447\u0430\u0441\u0442\u044c<\/a> \u043f\u043e\u0441\u0432\u044f\u0449\u0435\u043d\u0430 <code>Docker<\/code>, <code>Docker CLI<\/code> \u0438 <code>Dockerfile<\/code>;<\/li>\n<li>\u0432\u043e <a href=\"https:\/\/habr.com\/ru\/company\/timeweb\/blog\/597613\/\">\u0432\u0442\u043e\u0440\u043e\u0439 \u0447\u0430\u0441\u0442\u0438<\/a> \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043e <code>Docker Compose<\/code>.<\/li>\n<\/ul>\n<p>  <\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u043c\u044b \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u043c \u043f\u0440\u043e\u0441\u0442\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0441\u043e\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0438\u0437 \u0442\u0440\u0435\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432 \u0438 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445, \u0430 \u0432 \u0437\u0430\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u2014 &#171;\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0438\u0437\u0443\u0435\u043c&#187; \u0435\u0433\u043e.<\/p>\n<p>  <\/p>\n<p><a href=\"https:\/\/github.com\/harryheman\/docker-test\">\u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 \u0441 \u043a\u043e\u0434\u043e\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/a>.<\/p>\n<p>  <\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0430\u043c \u044d\u0442\u043e \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e, \u043f\u0440\u043e\u0448\u0443 \u043f\u043e\u0434 \u043a\u0430\u0442.<\/p>\n<p><a name=\"habracut\"><\/a>  <\/p>\n<h2 id=\"podgotovka-i-nastroyka-proekta\">\u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430<\/h2>\n<p>  <\/p>\n<p>\u041f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u0442\u0441\u044f, \u0447\u0442\u043e \u0432\u044b \u0445\u043e\u0442\u044f \u0431\u044b \u0432\u043a\u0440\u0430\u0442\u0446\u0435 \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u0438\u043b\u0438\u0441\u044c \u0441 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435\u043c \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0447\u0430\u0441\u0442\u0435\u0439 \u0438\u043b\u0438 \u0438\u0437\u0443\u0447\u0430\u043b\u0438 \u0434\u0440\u0443\u0433\u0438\u0435 \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b, \u043f\u043e\u0441\u0432\u044f\u0449\u0435\u043d\u043d\u044b\u0435 \u0440\u0430\u0431\u043e\u0442\u0435 \u0441 <a href=\"https:\/\/www.docker.com\/\"><code>Docker<\/code><\/a>. \u0412\u043f\u0440\u043e\u0447\u0435\u043c, \u0432 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 <code>Docker<\/code> \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0432\u0441\u0435\u043c \u0447\u0443\u0442\u044c-\u0447\u0443\u0442\u044c.<\/p>\n<p>  <\/p>\n<p>\u0422\u0430\u043a\u0436\u0435 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u0442\u0441\u044f, \u0447\u0442\u043e \u043d\u0430 \u0432\u0430\u0448\u0435\u0439 \u043c\u0430\u0448\u0438\u043d\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d <a href=\"https:\/\/www.docker.com\/products\/docker-desktop\"><code>Docker<\/code><\/a> \u0438 <a href=\"https:\/\/nodejs.org\/en\/download\/\"><code>Node.js<\/code><\/a>.<\/p>\n<p>  <\/p>\n<p>\u0425\u043e\u0440\u043e\u0448\u043e, \u0435\u0441\u043b\u0438 \u043d\u0430 \u0432\u0430\u0448\u0435\u0439 \u043c\u0430\u0448\u0438\u043d\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d <a href=\"https:\/\/yarnpkg.com\/\"><code>Yarn<\/code><\/a> \u0438 \u0432\u044b \u0438\u043c\u0435\u0435\u0442\u0435 \u043e\u043f\u044b\u0442 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 <a href=\"https:\/\/ru.reactjs.org\/\"><code>React<\/code><\/a>, <a href=\"https:\/\/vuejs.org\/\"><code>Vue<\/code><\/a>, <code>Node.js<\/code>, <a href=\"https:\/\/www.postgresql.org\/\"><code>PostgreSQL<\/code><\/a> \u0438 <a href=\"https:\/\/pubs.opengroup.org\/onlinepubs\/009604599\/utilities\/sh.html\">sh<\/a> \u0438\u043b\u0438 <a href=\"http:\/\/www.gnu.org\/software\/bash\/\"><code>bash<\/code><\/a> (\u0432\u0441\u0435 \u044d\u0442\u043e \u043e\u043f\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e).<\/p>\n<p>  <\/p>\n<p>\u041a\u0430\u043a \u044f \u0441\u043a\u0430\u0437\u0430\u043b, \u043d\u0430\u0448\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 \u0442\u0440\u0435\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432:<\/p>\n<p>  <\/p>\n<ul>\n<li>\u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043d\u0430 <code>React.js<\/code>;<\/li>\n<li>\u0430\u0434\u043c\u0438\u043d\u043a\u0438 \u043d\u0430 <code>Vue.js<\/code>;<\/li>\n<li>\u0441\u0435\u0440\u0432\u0435\u0440\u0430 (API) \u043d\u0430 <code>Node.js<\/code>.<\/li>\n<\/ul>\n<p>  <\/p>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <code>PostgreSQL<\/code>, \u0430 \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 \u043d\u0435\u0439 \u2014 <a href=\"https:\/\/www.prisma.io\/\">Prisma<\/a>.<\/p>\n<p>  <\/p>\n<p>\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c:<\/p>\n<p>  <\/p>\n<ul>\n<li>\u0432 \u0430\u0434\u043c\u0438\u043d\u043a\u0435 \u0437\u0430\u0434\u0430\u044e\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u043f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u044f, \u0442\u0435\u043c\u044b \u0438 \u0431\u0430\u0437\u043e\u0432\u043e\u0433\u043e \u0440\u0430\u0437\u043c\u0435\u0440\u0430 \u0448\u0440\u0438\u0444\u0442\u0430;<\/li>\n<li>\u044d\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u0411\u0414 \u0438 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u044e\u0442\u0441\u044f \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0435;<\/li>\n<li>\u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 &#171;\u0442\u0443\u0434\u0443\u0448\u043a\u0430&#187;;<\/li>\n<li>\u0437\u0430\u0434\u0430\u0447\u0438 \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u0411\u0414;<\/li>\n<li>\u0432\u0441\u0435 \u044d\u0442\u043e \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c.<\/li>\n<\/ul>\n<p>  <\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044e \u0434\u043b\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430, \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u043c \u0432 \u043d\u0435\u0435 \u0438 \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u0435\u0449\u0435 \u043f\u0430\u0440\u043e\u0447\u043a\u0443 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0439:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\">mkdir docker-test  cd !$ # docker-test  mkdir services sh uploads<\/code><\/pre>\n<p>  <\/p>\n<p>\u0412 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 <code>services<\/code> \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043d\u0430\u0448\u0438 \u0441\u0435\u0440\u0432\u0438\u0441\u044b, \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 <code>sh<\/code> \u2014 \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0434\u043b\u044f \u0442\u0435\u0440\u043c\u0438\u043d\u0430\u043b\u0430, \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044e <code>uploads<\/code> \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435 \u0431\u0443\u0434\u0435\u043c, \u043d\u043e \u043e\u0431\u044b\u0447\u043d\u043e \u0432 \u043d\u0435\u0439 \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0435 \u0444\u0430\u0439\u043b\u044b, \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c\u044b\u0435 \u0430\u0434\u043c\u0438\u043d\u043e\u043c \u0438\u043b\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c\u0438.<\/p>\n<p>  <\/p>\n<p>\u041f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u043c \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044e <code>services<\/code>, \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044e \u0434\u043b\u044f API, \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u043c \u0448\u0430\u0431\u043b\u043e\u043d \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/create-react-app.dev\/\"><code>Create React App<\/code><\/a> \u0438 \u0448\u0430\u0431\u043b\u043e\u043d \u0430\u0434\u043c\u0438\u043d\u043a\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/cli.vuejs.org\/\"><code>Vue CLI<\/code><\/a>:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\">cd services  mkdir api  yarn create react-app client # or npx create-react-app client  yarn create vue-app admin # or npx vue create admin<\/code><\/pre>\n<p>  <\/p>\n<p>\u041d\u0430\u0447\u043d\u0435\u043c \u0441 API.<\/p>\n<p>  <\/p>\n<h2 id=\"api\">API<\/h2>\n<p>  <\/p>\n<p>\u041f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u043c \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044e <code>api<\/code>, \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c <code>Node.js-\u043f\u0440\u043e\u0435\u043a\u0442<\/code> \u0438 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\">cd api  yarn init -yp # or npm init -y  # \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 yarn add express cors # \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 yarn add -D nodemon prisma<\/code><\/pre>\n<p>  <\/p>\n<ul>\n<li><a href=\"https:\/\/www.npmjs.com\/package\/express\">express<\/a> \u2014 <code>Node.js-\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a<\/code> \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432;<\/li>\n<li><a href=\"https:\/\/www.npmjs.com\/package\/cors\">cors<\/a> \u2014 \u0443\u0442\u0438\u043b\u0438\u0442\u0430 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 <a href=\"https:\/\/developer.mozilla.org\/ru\/docs\/Web\/HTTP\/CORS\">CORS<\/a>;<\/li>\n<li><a href=\"https:\/\/www.npmjs.com\/package\/nodemon\">nodemon<\/a> \u2014 \u0443\u0442\u0438\u043b\u0438\u0442\u0430 \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438;<\/li>\n<li><a href=\"https:\/\/www.npmjs.com\/package\/prisma\">prisma<\/a> \u2014 \u044f\u0434\u0440\u043e (core) <a href=\"https:\/\/ru.wikipedia.org\/wiki\/ORM\">ORM<\/a>, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 <code>postgres<\/code>.<\/li>\n<\/ul>\n<p>  <\/p>\n<p>\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c <code>prisma<\/code>:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\">npx prisma init<\/code><\/pre>\n<p>  <\/p>\n<p>\u042d\u0442\u043e \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 <code>prisma<\/code>, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0444\u0430\u0439\u043b\u043e\u0432 <code>prisma\/schema.prisma<\/code> \u0438 <code>.env<\/code>.<\/p>\n<p>  <\/p>\n<p>\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u0433\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440, \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u043c\u043e\u0434\u0435\u043b\u0438 \u0432 \u0444\u0430\u0439\u043b\u0435 <code>prisma\/schema.prisma<\/code>:<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">\/\/ https:\/\/pris.ly\/d\/prisma-schema generator client {   provider      = \"prisma-client-js\"   \/\/ \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430   binaryTargets = [\"native\"] }  datasource db {   provider = \"postgresql\"   \/\/ \u043f\u0443\u0442\u044c \u043a \u0411\u0414 \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442\u0441\u044f \u0438\u0437 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0441\u0440\u0435\u0434\u044b \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f `DATABASE_URL`   url      = env(\"DATABASE_URL\") }  \/\/ \u043c\u043e\u0434\u0435\u043b\u044c \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a model Settings {   id             Int      @id @default(autoincrement())   created_at     DateTime @default(now())   updated_at     DateTime @updatedAt   greetings      String   theme          String   base_font_size String }  \/\/ \u043c\u043e\u0434\u0435\u043b\u044c \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u0447\u0438 model Todo {   id         Int      @id @default(autoincrement())   created_at DateTime @default(now())   updated_at DateTime @updatedAt   text       String   done       Boolean }<\/code><\/pre>\n<p>  <\/p>\n<p>\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u043f\u0443\u0442\u044c \u043a \u0411\u0414 \u0432 \u0444\u0430\u0439\u043b\u0435 <code>.env<\/code>:<\/p>\n<p>  <\/p>\n<pre><code class=\"plaintext\">DATABASE_URL=postgresql:\/\/postgres:postgres@localhost:5432\/mydb?schema=public<\/code><\/pre>\n<p>  <\/p>\n<p>\u0417\u0434\u0435\u0441\u044c:<\/p>\n<p>  <\/p>\n<ul>\n<li><code>postgres<\/code> \u2014 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c;<\/li>\n<li><code>localhost<\/code> \u2014 \u0445\u043e\u0441\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u043f\u0443\u0449\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440 <code>postgres<\/code>;<\/li>\n<li><code>5432<\/code> \u2014 \u043f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u043f\u0443\u0449\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440 <code>postgres<\/code>;<\/li>\n<li><code>mydb<\/code> \u2014 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0411\u0414.<\/li>\n<\/ul>\n<p>  <\/p>\n<p>\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u043a\u043e\u043c\u0430\u043d\u0434\u0443 \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 <a href=\"https:\/\/hub.docker.com\/_\/postgres\"><code>\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 postgres<\/code><\/a> \u0432 \u0444\u0430\u0439\u043b\u0435 <code>sh\/db<\/code> (\u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f):<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\">docker run --rm --name postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_DB=mydb -dp 5432:5432 -v $HOME\/docker\/volumes\/postgres:\/var\/lib\/postgresql\/data postgres<\/code><\/pre>\n<p>  <\/p>\n<p><em>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435<\/em>: \u0435\u0441\u043b\u0438 \u0432\u044b \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442\u0435 \u043d\u0430 <code>Mac<\/code>, \u0432\u0430\u043c \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0430\u043c\u043e\u043c\u0443 \u0441\u0435\u0431\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u043a\u043e\u0434\u0430 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 <code>sh\/db<\/code>. \u042d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0442\u0430\u043a:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\"># \u043c\u044b \u043d\u0430\u0445\u043e\u0434\u0438\u043c\u0441\u044f \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 `sh` chmod +x db # or sudo chmod +x db<\/code><\/pre>\n<p>  <\/p>\n<p>\u041d\u0430\u0445\u043e\u0434\u044f\u0441\u044c \u0432 \u043a\u043e\u0440\u043d\u0435\u0432\u043e\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u0430, \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u043c \u0442\u0435\u0440\u043c\u0438\u043d\u0430\u043b \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u043a\u043e\u043c\u0430\u043d\u0434\u0443:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\">sh\/db<\/code><\/pre>\n<p>  <\/p>\n<p>\u041f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u043e\u0431\u0440\u0430\u0437\u0430 <code>postgres<\/code> \u0438\u0437 <a href=\"https:\/\/hub.docker.com\/\"><code>Docker Hub<\/code><\/a> \u0438 \u0437\u0430\u043f\u0443\u0441\u043a \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u043f\u043e\u0434 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c <code>postgres<\/code>.<\/p>\n<p>  <\/p>\n<p><em>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435<\/em>: \u0438\u043d\u043e\u0433\u0434\u0430 \u043c\u043e\u0436\u0435\u0442 \u0432\u043e\u0437\u043d\u0438\u043a\u043d\u0443\u0442\u044c \u043e\u0448\u0438\u0431\u043a\u0430, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u0430\u044f \u0441 \u0442\u0435\u043c, \u0447\u0442\u043e \u043f\u043e\u0440\u0442 5432 \u0437\u0430\u043d\u044f\u0442 \u0434\u0440\u0443\u0433\u0438\u043c \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u043c. \u0412 \u044d\u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043d\u0430\u0439\u0442\u0438 <code>PID<\/code> \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0438 &#171;\u0443\u0431\u0438\u0442\u044c&#187; \u0435\u0433\u043e. \u041d\u0430 <code>Mac<\/code> \u044d\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442\u0441\u044f \u0442\u0430\u043a:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\"># \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c `PID` \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430, \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u043e\u0433\u043e \u043d\u0430 \u043f\u043e\u0440\u0442\u0443 `5432` sudo lsof -i :5432 # \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0438\u043c, \u0447\u0442\u043e `PID` \u0438\u043c\u0435\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 `103` # \"\u0443\u0431\u0438\u0432\u0430\u0435\u043c\" \u043f\u0440\u043e\u0446\u0435\u0441\u0441 sudo kill 103<\/code><\/pre>\n<p>  <\/p>\n<p>\u0423\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u043c\u043e\u0436\u043d\u043e, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0443 <code>docker ps<\/code>:<\/p>\n<p>  <img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/qz\/cb\/jn\/qzcbjnikl7acjtp3u90sd1xcf2o.png\"\/>  <\/p>\n<p>  <\/p>\n<p>  <\/p>\n<p>\u0418\u043b\u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0432 <a href=\"https:\/\/www.docker.com\/products\/docker-desktop\"><code>Docker Desktop<\/code><\/a>:<\/p>\n<p>  <img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/xr\/zu\/n9\/xrzun9hxa_zlwwvueumwjzyzq98.png\"\/>  <\/p>\n<p>  <\/p>\n<p>  <\/p>\n<p>\u0418\u043b\u0438 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 <code>Individual Containers<\/code> <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=ms-azuretools.vscode-docker\"><code>\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f Docker<\/code> \u0434\u043b\u044f <code>VSCode<\/code><\/a>:<\/p>\n<p>  <img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/1l\/e0\/96\/1le096w7kcbze_0outprie0ua1g.png\"\/>  <\/p>\n<p>  <\/p>\n<p>  <\/p>\n<p>\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044e:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\"># \u043c\u044b \u043d\u0430\u0445\u043e\u0434\u0438\u043c\u0441\u044f \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 `api` # migrate dev - \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 # --name init - \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 npx prisma migrate dev --name init<\/code><\/pre>\n<p>  <\/p>\n<p>\u042d\u0442\u043e \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0444\u0430\u0439\u043b\u0430 <code>prisma\/migrations\/[Date]-init\/migration.sql<\/code>, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044e \u043a \u0411\u0414, \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u0432 \u043d\u0435\u0439 \u0442\u0430\u0431\u043b\u0438\u0446, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 <a href=\"https:\/\/www.npmjs.com\/package\/@prisma\/client\"><code>@prisma\/client<\/code><\/a>.<\/p>\n<p>  <\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <code>prisma\/seed.js<\/code> \u0441 \u043a\u043e\u0434\u043e\u043c \u0434\u043b\u044f \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0411\u0414 \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438:<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">import Prisma from '@prisma\/client' const { PrismaClient } = Prisma  \/\/ \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u0430 const prisma = new PrismaClient()  \/\/ \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 const initialSettings = {   greetings: 'Welcome to Docker Test App',   theme: 'light',   base_font_size: '16px' }  \/\/ \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u044b\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 const initialTodos = [   {     text: 'Eat',     done: true   },   {     text: 'Code',     done: true   },   {     text: 'Sleep',     done: false   },   {     text: 'Repeat',     done: false   } ]  async function main() {   try {     \/\/ \u0435\u0441\u043b\u0438 \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0443\u0441\u0442\u043e\u0439     if (!(await prisma.settings.findFirst())) {       await prisma.settings.create({ data: initialSettings })     }     \/\/ \u0435\u0441\u043b\u0438 \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u0437\u0430\u0434\u0430\u0447 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0443\u0441\u0442\u043e\u0439     if (!(await (await prisma.todo.findMany()).length)) {       await prisma.todo.createMany({ data: initialTodos })     }     console.log('Database has been successfully seeded ? ')   } catch (e) {     console.log(e)   } finally {     await prisma.$disconnect()   } }  main()<\/code><\/pre>\n<p>  <\/p>\n<p>\u0412 <code>package.json<\/code> \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u0442\u0438\u043f \u043a\u043e\u0434\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u043c\u043e\u0434\u0443\u043b\u044c), \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u0443 \u0434\u043b\u044f \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0411\u0414 \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438:<\/p>\n<p>  <\/p>\n<pre><code class=\"json\">\"type\": \"module\", \"scripts\": {   \"dev\": \"nodemon\",   \"start\": \"prisma generate &amp;&amp; prisma migrate deploy &amp;&amp; node index.js\" }, \"prisma\": {   \"seed\": \"node prisma\/seed.js\" }<\/code><\/pre>\n<p>  <\/p>\n<p>\u0417\u0430\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u0411\u0414 \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\"># \u043c\u044b \u043d\u0430\u0445\u043e\u0434\u0438\u043c\u0441\u044f \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 `api` npx prisma db seed<\/code><\/pre>\n<p>  <\/p>\n<p>\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u043c \u043d\u0430\u0448\u0443 \u0411\u0414 \u0432 \u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\">npx prisma studio<\/code><\/pre>\n<p>  <\/p>\n<p>\u042d\u0442\u043e \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u044e \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 <code>http:\/\/localhost:5555<\/code>:<\/p>\n<p>  <img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/6-\/ar\/y1\/6-ary1tso3uyjb0imkqlwg4aw94.png\"\/>  <\/p>\n<p>  <\/p>\n<p>  <\/p>\n<p>\u041f\u0440\u0438\u0441\u0442\u0443\u043f\u0430\u0435\u043c \u043a \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430.<\/p>\n<p>  <\/p>\n<p>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439:<\/p>\n<p>  <\/p>\n<pre><code class=\"plaintext\">- routes   - index.js   - settings.routes.js - \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b (\u0440\u043e\u0443\u0442\u044b) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a   - todo.routes.js - \u0440\u043e\u0443\u0442\u044b \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u0447 - index.js<\/code><\/pre>\n<p>  <\/p>\n<p>\u0421\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u0430 <code>index.js<\/code>:<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">\/\/ \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0438 \u0443\u0442\u0438\u043b\u0438\u0442\u044b import express from 'express' import cors from 'cors' import Prisma from '@prisma\/client' import apiRoutes from '.\/routes\/index.js' import { join, dirname } from 'path' import { fileURLToPath } from 'url' const { PrismaClient } = Prisma  \/\/ \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u0438 \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 `prisma` export const prisma = new PrismaClient()  \/\/ \u043f\u0443\u0442\u044c \u043a \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 const __dirname = dirname(fileURLToPath(import.meta.url))  \/\/ \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f `express` const app = express()  \/\/ \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u043c `cors` app.use(cors()) \/\/ \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u043c \u043f\u0430\u0440\u0441\u0438\u043d\u0433 `json` \u0432 \u043e\u0431\u044a\u0435\u043a\u0442\u044b app.use(express.json())  \/\/ \u044d\u0442\u043e \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430\u043c \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435 if (process.env.ENV === 'prod') {   \/\/ \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u043f\u0443\u0442\u0438   \/\/ \u043f\u0443\u0442\u044c \u043a \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 + `client\/build`   const clientBuildPath = join(__dirname, 'client', 'build')   \/\/ \u043f\u0443\u0442\u044c \u043a \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 + `admin\/dist`   const adminDistPath = join(__dirname, 'admin', 'dist')    \/\/ \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0444\u0430\u0439\u043b\u043e\u0432   \/\/ \u043a\u043b\u0438\u0435\u043d\u0442 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u043f\u043e \u043f\u0443\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430   app.use(express.static(clientBuildPath))   app.use(express.static(adminDistPath))   \/\/ \u0430\u0434\u043c\u0438\u043d\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043f\u043e \u043f\u0443\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 + `\/admin`   app.use('\/admin', (req, res) => {     res.sendFile(join(adminDistPath, decodeURIComponent(req.url)))   }) } \/\/ \u0440\u043e\u0443\u0442\u0438\u043d\u0433 app.use('\/api', apiRoutes)  \/\/ \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043e\u0448\u0438\u0431\u043e\u043a app.use((err, req, res, next) => {   console.log(err)   const status = err.status || 500   const message = err.message || 'Something went wrong. Try again later'   res.status(status).json({ message }) })  \/\/ \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442\u0443 5000 app.listen(5000, () => {   console.log(`Server ready ? `) })<\/code><\/pre>\n<p>  <\/p>\n<p>\u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0440\u043e\u0443\u0442\u044b.<\/p>\n<p>  <\/p>\n<p>\u041d\u0430\u0447\u043d\u0435\u043c \u0441 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f (<code>routes\/index.js<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">import { Router } from 'express' import todoRoutes from '.\/todo.routes.js' import settingsRoutes from '.\/settings.routes.js'  const router = Router()  router.use('\/todo', todoRoutes) router.use('\/settings', settingsRoutes)  export default router<\/code><\/pre>\n<p>  <\/p>\n<p>\u0420\u043e\u0443\u0442\u0435\u0440 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a (<code>routes\/settings.routes.js<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">import { Router } from 'express' import { prisma } from '..\/index.js'  const router = Router()  \/\/ \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a router.get('\/', async (req, res, next) => {   try {     const settings = await prisma.settings.findFirst()     res.status(200).json(settings)   } catch (e) {     next(e)   } })  \/\/ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a router.put('\/:id', async (req, res, next) => {   const id = Number(req.params.id)   try {     const settings = await prisma.settings.update({       data: req.body,       where: { id }     })     res.status(201).json(settings)   } catch (e) {     next(e)   } })  export default router<\/code><\/pre>\n<p>  <\/p>\n<p>\u0420\u043e\u0443\u0442\u0435\u0440 \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u0447 (<code>routes\/todo.routes.js<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">import { Router } from 'express' import { prisma } from '..\/index.js'  const router = Router()  \/\/ \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447 router.get('\/', async (req, res, next) => {   try {     const todos = (await prisma.todo.findMany()).sort(       (a, b) => a.created_at - b.created_at     )     res.status(200).json(todos)   } catch (e) {     next(e)   } })  \/\/ \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 router.post('\/', async (req, res, next) => {   try {     const newTodo = await prisma.todo.create({       data: req.body     })     res.status(201).json(newTodo)   } catch (e) {     next(e)   } })  \/\/ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 router.put('\/:id', async (req, res, next) => {   const id = Number(req.params.id)   try {     const updatedTodo = await prisma.todo.update({       data: req.body,       where: { id }     })     res.status(201).json(updatedTodo)   } catch (e) {     next(e)   } })  \/\/ \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 router.delete('\/:id', async (req, res, next) => {   const id = Number(req.params.id)   try {     await prisma.todo.delete({       where: { id }     })     res.sendStatus(201)   } catch (e) {     next(e)   } })  export default router<\/code><\/pre>\n<p>  <\/p>\n<p>\u042d\u0442\u043e \u0432\u0441\u0435, \u0447\u0442\u043e \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043e\u0442 \u043d\u0430\u0448\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430.<\/p>\n<p>  <\/p>\n<h2 id=\"adminka\">\u0410\u0434\u043c\u0438\u043d\u043a\u0430<\/h2>\n<p>  <\/p>\n<p>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0430\u0434\u043c\u0438\u043d\u043a\u0438 \u0431\u0443\u0434\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 (<code>admin\/src<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"plaintext\">- components   - App.vue - \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f   - Settings.vue - \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a - index.js<\/code><\/pre>\n<p>  <\/p>\n<p>\u041d\u0430\u0447\u043d\u0435\u043c \u0441 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 (<code>components\/App.vue<\/code>).<\/p>\n<p>  <\/p>\n<p>\u0420\u0430\u0437\u043c\u0435\u0442\u043a\u0430:<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">&lt;template>   &lt;div id=\"app\">     &lt;h1>Admin&lt;\/h1>     &lt;!-- \u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 -->     &lt;h2 v-if=\"loading\">Loading...&lt;\/h2>     &lt;!-- \u041e\u0448\u0438\u0431\u043a\u0430 -->     &lt;h3 v-else-if=\"error\" class=\"error\">       {{ error.message || 'Something went wrong. Try again later' }}     &lt;\/h3>     &lt;!-- \u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a -->     &lt;div v-else>       &lt;h2>Settings&lt;\/h2>       &lt;!-- \u041f\u0440\u043e\u043f\u044b: \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0435 \u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (\u0438\u0437 \u0411\u0414), \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0438\u0445 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438 \u0430\u0434\u0440\u0435\u0441 API -->       &lt;Settings :settings=\"settings\" :getSettings=\"getSettings\" :apiUri=\"apiUri\" \/>     &lt;\/div>   &lt;\/div> &lt;\/template><\/code><\/pre>\n<p>  <\/p>\n<p>\u0421\u0442\u0438\u043b\u0438:<\/p>\n<p>  <\/p>\n<pre><code class=\"css\">@import url('https:\/\/fonts.googleapis.com\/css2?family=Montserrat:wght@200;400;600&amp;display=swap');  :root {   --primary: #0275d8;   --success: #5cb85c;   --warning: #f0ad4e;   --danger: #d9534f;   --light: #f7f7f7;   --dark: #292b2c; }  * {   font-family: 'Montserrat', sans-serif;   font-size: 1rem; }  body.light {   background-color: var(--light);   color: var(--dark); }  body.dark {   background-color: var(--dark);   color: var(--light); }  #app {   display: flex;   flex-direction: column;   text-align: center; }  h2 {   font-size: 1.4rem; }  form div {   display: flex;   flex-direction: column;   align-items: center; }  label {   margin: 0.5rem 0; }  input {   padding: 0.5rem;   max-width: 220px;   width: max-content;   outline: none;   border: 1px solid var(--dark);   border-radius: 4px;   text-align: center; }  input:focus {   border-color: var(--primary); }  button {   margin: 1rem 0;   padding: 0.5rem 1rem;   background: none;   border: none;   border-radius: 4px;   outline: none;   background-color: var(--success);   color: var(--light);   box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);   cursor: pointer;   user-select: none;   transition: 0.2s; }  button:active {   box-shadow: none; }  .error {   color: var(--danger); }<\/code><\/pre>\n<p>  <\/p>\n<p>\u0421\u043a\u0440\u0438\u043f\u0442:<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">\/\/ \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a import Settings from '.\/Settings'  export default {   \/\/ \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430   name: 'App',   \/\/ \u0434\u043e\u0447\u0435\u0440\u043d\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b   components: {     Settings   },   \/\/ \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435   data() {     return {       loading: true,       error: null,       settings: {},       apiUri: 'http:\/\/localhost:5000\/api\/settings'     }   },   \/\/ \u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430   created() {     \/\/ \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438     this.getSettings()   },   \/\/ \u043c\u0435\u0442\u043e\u0434\u044b   methods: {     \/\/ \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a     async getSettings() {       this.loading = true       try {         const response = await fetch(API_URI)         if (!response.ok) throw response         this.settings = await response.json()       } catch (e) {         this.error = e       } finally {         this.loading = false       }     }   } }<\/code><\/pre>\n<p>  <\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a (<code>components\/Settings.vue<\/code>).<\/p>\n<p>  <\/p>\n<p>\u0420\u0430\u0437\u043c\u0435\u0442\u043a\u0430:<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">&lt;template>   &lt;!-- \u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 -->   &lt;div v-if=\"loading\">Loading...&lt;\/div>   &lt;!-- \u041e\u0448\u0438\u0431\u043a\u0430 -->   &lt;div v-else-if=\"error\">     {{ error.message || JSON.stringify(error, null, 2) }}   &lt;\/div>   &lt;!-- \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -->   &lt;form v-else @submit.prevent=\"saveSettings\">     &lt;!-- \u041f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435 -->     &lt;div>       &lt;label for=\"greetings\">Greetings&lt;\/label>       &lt;input         type=\"text\"         id=\"greetings\"         name=\"greetings\"         :value=\"settings.greetings\"         required       \/>     &lt;\/div>     &lt;!-- \u0422\u0435\u043c\u0430 -->     &lt;div>       &lt;label for=\"theme\">Theme&lt;\/label>       &lt;input         type=\"text\"         id=\"theme\"         name=\"theme\"         :value=\"settings.theme\"         required       \/>     &lt;\/div>     &lt;!-- \u0411\u0430\u0437\u043e\u0432\u044b\u0439 \u0440\u0430\u0437\u043c\u0435\u0440 \u0448\u0440\u0438\u0444\u0442\u0430 -->     &lt;div>       &lt;label for=\"base_font_size\">Base font size&lt;\/label>       &lt;input         type=\"text\"         id=\"base_font_size\"         name=\"base_font_size\"         :value=\"settings.base_font_size\"         required       \/>     &lt;\/div>      &lt;button>Save&lt;\/button>   &lt;\/form> &lt;\/template><\/code><\/pre>\n<p>  <\/p>\n<p>\u0421\u043a\u0440\u0438\u043f\u0442:<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">export default {   \/\/ \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430   name: 'Settings',   \/\/ \u043f\u0440\u043e\u043f\u044b   props: {     settings: {       type: Object,       required: true     },     getSettings: {       type: Function,       required: true     },     apiUri: {       type: String,       required: true     }   },   \/\/ \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435   data() {     return {       loading: false,       error: null     }   },   \/\/ \u043c\u0435\u0442\u043e\u0434\u044b   methods: {     \/\/ \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0432 \u0411\u0414     async saveSettings(e) {       this.loading = true       const formDataObj = [...new FormData(e.target)].reduce(         (obj, [key, val]) => ({           ...obj,           [key]: val         }),         {}       )       try {         const response = await fetch(`${this.apiUri}\/${this.settings.id}`, {           method: 'PUT',           body: JSON.stringify(formDataObj),           headers: {             'Content-Type': 'application\/json'           }         })         if (!response.ok) throw response         \/\/ \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438         await this.getSettings()       } catch (e) {         this.error = e       } finally {         this.loading = false       }     }   } }<\/code><\/pre>\n<p>  <\/p>\n<p>\u041d\u0430 \u044d\u0442\u043e\u043c \u0441 \u0430\u0434\u043c\u0438\u043d\u043a\u043e\u0439 \u043c\u044b \u0437\u0430\u043a\u043e\u043d\u0447\u0438\u043b\u0438.<\/p>\n<p>  <\/p>\n<h2 id=\"klient\">\u041a\u043b\u0438\u0435\u043d\u0442<\/h2>\n<p>  <\/p>\n<p>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0431\u0443\u0434\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 (<code>client\/src<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"plaintext\">- api   - settings.api.js - API \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a   - todo.api.js - API \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u0447 - components   - TodoForm.js - \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447\u0438   - TodoList.js - \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u0437\u0430\u0434\u0430\u0447 - hooks   - useStore.js - \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0432 \u0432\u0438\u0434\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0445\u0443\u043a\u0430 - App.js - \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f - App.css - index.js<\/code><\/pre>\n<p>  <\/p>\n<p>\u0414\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href=\"https:\/\/github.com\/harryheman\/React-Total\/blob\/main\/md\/zustand.md\"><code>Zustand<\/code><\/a>.<\/p>\n<p>  <\/p>\n<p>\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0435\u0433\u043e:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\">yarn add zustand<\/code><\/pre>\n<p>  <\/p>\n<p>\u041d\u0430\u0447\u043d\u0435\u043c \u0441 API \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a (<code>api\/settings.api.js<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">\/\/ \u043a\u043e\u043d\u0435\u0447\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430 const API_URI = 'http:\/\/localhost:5000\/api\/settings'  \/\/ \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a const fetchSettings = async () => {   try {     const response = await fetch(API_URI)     if (!response.ok) throw response     return await response.json()   } catch (e) {     throw e   } }  const settingsApi = { fetchSettings }  export default settingsApi<\/code><\/pre>\n<p>  <\/p>\n<p>API \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u0447 (<code>api\/todo.api.js<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">\/\/ \u043a\u043e\u043d\u0435\u0447\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430 const API_URI = 'http:\/\/localhost:5000\/api\/todo'  \/\/ \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447 const fetchTodos = async () => {   try {     const response = await fetch(API_URI)     if (!response.ok) throw response     return await response.json()   } catch (e) {     throw e   } }  \/\/ \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 const addTodo = async (newTodo) => {   try {     const response = await fetch(API_URI, {       method: 'POST',       body: JSON.stringify(newTodo),       headers: {         'Content-Type': 'application\/json'       }     })     if (!response.ok) throw response     return await response.json()   } catch (e) {     throw e   } }  \/\/ \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447\u0438 const updateTodo = async (id, changes) => {   try {     const response = await fetch(`${API_URI}\/${id}`, {       method: 'PUT',       body: JSON.stringify(changes),       headers: {         'Content-Type': 'application\/json'       }     })     if (!response.ok) throw response     return await response.json()   } catch (e) {     throw e   } }  \/\/ \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447\u0438 const removeTodo = async (id) => {   try {     const response = await fetch(`${API_URI}\/${id}`, {       method: 'DELETE'     })     if (!response.ok) throw response   } catch (e) {     throw e   } }  const todoApi = { fetchTodos, addTodo, updateTodo, removeTodo }  export default todoApi<\/code><\/pre>\n<p>  <\/p>\n<p>\u0425\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0432\u0438\u0434\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0445\u0443\u043a\u0430 (<code>hooks\/useStore.js<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">import create from 'zustand' \/\/ API \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a import settingsApi from '..\/api\/settings.api' \/\/ API \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u0447 import todoApi from '..\/api\/todo.api'  const useStore = create((set, get) => ({   \/\/ \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435   settings: {},   todos: [],   loading: false,   error: null,   \/\/ \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f   \/\/ \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a   fetchSettings() {     set({ loading: true })     settingsApi       .fetchSettings()       .then((settings) => {         set({ settings })       })       .catch((error) => {         set({ error })       })       .finally(() => {         set({ loading: false })       })   },   \/\/ \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447   fetchTodos() {     set({ loading: true })     todoApi       .fetchTodos()       .then((todos) => {         set({ todos })       })       .catch((error) => {         set({ error })       })       .finally(() => {         set({ loading: false })       })   },   \/\/ \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447\u0438   addTodo(newTodo) {     set({ loading: true })     todoApi       .addTodo(newTodo)       .then((newTodo) => {         const todos = [...get().todos, newTodo]         set({ todos })       })       .catch((error) => {         set({ error })       })       .finally(() => {         set({ loading: false })       })   },   \/\/ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447\u0438   updateTodo(id, changes) {     set({ loading: true })     todoApi       .updateTodo(id, changes)       .then((updatedTodo) => {         const todos = get().todos.map((todo) =>           todo.id === updatedTodo.id ? updatedTodo : todo         )         set({ todos })       })       .catch((error) => {         set({ error })       })       .finally(() => {         set({ loading: false })       })   },   \/\/ \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447\u0438   removeTodo(id) {     set({ loading: true })     todoApi       .removeTodo(id)       .then(() => {         const todos = get().todos.filter((todo) => todo.id !== id)         set({ todos })       })       .catch((error) => {         set({ error })       })       .finally(() => {         set({ loading: false })       })   } }))  export default useStore<\/code><\/pre>\n<p>  <\/p>\n<p>\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 (<code>components\/TodoForm.js<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">import { useState, useEffect } from 'react' import useStore from '..\/hooks\/useStore'  export default function TodoForm() {   \/\/ \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430   const addTodo = useStore(({ addTodo }) => addTodo)   \/\/ \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0442\u0435\u043a\u0441\u0442\u0430 \u043d\u043e\u0432\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438   const [text, setText] = useState('')   const [disable, setDisable] = useState(true)    useEffect(() => {     setDisable(!text.trim())   }, [text])    \/\/ \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0442\u0435\u043a\u0441\u0442\u0430 \u0437\u0430\u0434\u0430\u0447\u0438   const onChange = ({ target: { value } }) => {     setText(value)   }    \/\/ \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0444\u043e\u0440\u043c\u044b   const onSubmit = (e) => {     e.preventDefault()     if (disable) return     const newTodo = {       text,       done: false     }     addTodo(newTodo)   }    return (     &lt;form onSubmit={onSubmit}>       &lt;label htmlFor='text'>New todo text&lt;\/label>       &lt;input type='text' id='text' value={text} onChange={onChange} \/>       &lt;button className='add'>Add&lt;\/button>     &lt;\/form>   ) }<\/code><\/pre>\n<p>  <\/p>\n<p>\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u0437\u0430\u0434\u0430\u0447 (<code>components\/TodoList.js<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">import useStore from '..\/hooks\/useStore'  export default function TodoList() {   \/\/ \u0437\u0430\u0434\u0430\u0447\u0438 \u0438 \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430   const { todos, updateTodo, removeTodo } = useStore(     ({ todos, updateTodo, removeTodo }) => ({ todos, updateTodo, removeTodo })   )    return (     &lt;ul>       {todos.map(({ id, text, done }) => (         &lt;li key={id}>           &lt;input             type='checkbox'             checked={done}             onChange={() => {               updateTodo(id, { done: !done })             }}           \/>           &lt;span>{text}&lt;\/span>           &lt;button             onClick={() => {               removeTodo(id)             }}           >             Remove           &lt;\/button>         &lt;\/li>       ))}     &lt;\/ul>   ) }<\/code><\/pre>\n<p>  <\/p>\n<p>\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f (<code>App.js<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"javascript\">import { useEffect } from 'react' import '.\/App.css' import useStore from '.\/hooks\/useStore' import TodoForm from '.\/components\/TodoForm' import TodoList from '.\/components\/TodoList'  \/\/ \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 useStore.getState().fetchSettings() \/\/ \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0437\u0430\u0434\u0430\u0447\u0438 useStore.getState().fetchTodos()  function App() {   \/\/ \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438, \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0438\u0437 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430   const { settings, loading, error } = useStore(     ({ settings, loading, error }) => ({ settings, loading, error })   )    useEffect(() => {     if (Object.keys(settings).length) {       \/\/ \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c \u0431\u0430\u0437\u043e\u0432\u044b\u0439 \u0440\u0430\u0437\u043c\u0435\u0440 \u0448\u0440\u0438\u0444\u0442\u0430 \u043a \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443 `html`       document.documentElement.style.fontSize = settings.base_font_size       \/\/ \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c \u0442\u0435\u043c\u0443       document.body.className = settings.theme     }   }, [settings])    \/\/ \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430   if (loading) return &lt;h2>Loading...&lt;\/h2>    \/\/ \u043e\u0448\u0438\u0431\u043a\u0430   if (error)     return (       &lt;h3 className='error'>         {error.message || 'Something went wrong. Try again later'}       &lt;\/h3>     )    return (     &lt;div className='App'>       &lt;h1>Client&lt;\/h1>       &lt;h2>{settings.greetings}&lt;\/h2>       &lt;TodoForm \/>       &lt;TodoList \/>     &lt;\/div>   ) }  export default App<\/code><\/pre>\n<p>  <\/p>\n<p>\u0421\u0442\u0438\u043b\u0438 (<code>App.css<\/code>):<\/p>\n<p>  <\/p>\n<pre><code class=\"css\">@import url('https:\/\/fonts.googleapis.com\/css2?family=Montserrat:wght@200;400;600&amp;display=swap');  :root {   --primary: #0275d8;   --success: #5cb85c;   --warning: #f0ad4e;   --danger: #d9534f;   --light: #f7f7f7;   --dark: #292b2c; }  * {   margin: 0;   padding: 0;   box-sizing: border-box;   font-family: 'Montserrat', sans-serif;   font-size: 1rem; }  \/* \u0422\u0435\u043c\u0430 *\/ body.light {   background-color: var(--light);   color: var(--dark); }  body.dark {   background-color: var(--dark);   color: var(--light); } \/* --- *\/  #root {   padding: 1rem;   display: flex;   justify-content: center; }  .App {   display: flex;   flex-direction: column;   align-items: center; }  h1, h2 {   margin: 1rem 0; }  h1 {   font-size: 1.6rem; }  h2 {   font-size: 1.4rem; }  h3 {   font-size: 1.2rem; }  label {   margin-bottom: 0.5rem;   display: block; }  form {   margin: 1rem 0; }  form input {   padding: 0.5rem;   max-width: 220px;   width: max-content;   outline: none;   border: 1px solid var(--dark);   border-radius: 4px;   text-align: center; }  form input:focus {   border-color: var(--primary); }  ul {   list-style: none; }  li {   margin: 0.75rem 0;   display: flex;   align-items: center;   justify-content: space-between; }  li input {   width: 18px;   height: 18px; }  li span {   display: block;   width: 120px;   word-break: break-all; }  button {   padding: 0.5rem 1rem;   background: none;   border: none;   border-radius: 4px;   outline: none;   background-color: var(--danger);   color: var(--light);   box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);   cursor: pointer;   user-select: none;   transition: 0.2s; }  button:active {   box-shadow: none; }  button.add {   background-color: var(--success); }  .error {   color: var(--danger); }  .App {   text-align: center; }<\/code><\/pre>\n<p>  <\/p>\n<p>\u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u0442\u0441\u0442\u0443\u043f\u044b \u0438 \u0440\u0430\u0437\u043c\u0435\u0440\u044b \u0437\u0430\u0434\u0430\u043d\u044b \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/webref.ru\/css\/unit\/rem\">rem<\/a>, \u043c\u044b \u043b\u0435\u0433\u043a\u043e \u043c\u043e\u0436\u0435\u043c \u043c\u0430\u043d\u0438\u043f\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0438\u043c\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c\u0438, \u043c\u0435\u043d\u044f\u044f \u0440\u0430\u0437\u043c\u0435\u0440 \u0448\u0440\u0438\u0444\u0442\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 <code>html<\/code>.<\/p>\n<p>  <\/p>\n<p>\u041d\u0430 \u044d\u0442\u043e\u043c \u0441 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043c \u043c\u044b \u0442\u0430\u043a\u0436\u0435 \u0437\u0430\u043a\u043e\u043d\u0447\u0438\u043b\u0438.<\/p>\n<p>  <\/p>\n<h2 id=\"proverka-rabotosposobnosti-prilozheniya\">\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h2>\n<p>  <\/p>\n<p>\u041f\u043e\u0434\u043d\u0438\u043c\u0430\u0435\u043c\u0441\u044f \u0432 \u043a\u043e\u0440\u043d\u0435\u0432\u0443\u044e \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044e (<code>docker-test<\/code>), \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c <code>Node.js-\u043f\u0440\u043e\u0435\u043a\u0442<\/code> \u0438 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c <a href=\"https:\/\/www.npmjs.com\/package\/concurrently\"><code>concurrently<\/code><\/a> \u2014 \u0443\u0442\u0438\u043b\u0438\u0442\u0443 \u0434\u043b\u044f \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043a\u043e\u043c\u0430\u043d\u0434, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0432 \u0444\u0430\u0439\u043b\u0435 <code>package.json<\/code>:<\/p>\n<p>  <\/p>\n<pre><code class=\"bash\"># \u043c\u044b \u043d\u0430\u0445\u043e\u0434\u0438\u043c\u0441\u044f \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 `docker-test` yarn init -yp yarn add concurrently<\/code><\/pre>\n<p>  <\/p>\n<p>\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432 \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0432 <code>package.json<\/code>:<\/p>\n<p>  <\/p>\n<pre><code class=\"json\">\"scripts\": {   \"dev:client\": \"yarn --cwd services\/client start\",   \"dev:admin\": \"yarn --cwd services\/admin dev\",   \"dev:api\": \"yarn --cwd services\/api dev\",   \"dev\": \"concurrently \\\"yarn dev:client\\\" \\\"yarn dev:admin\\\" \\\"yarn dev:api\\\"\" }<\/code><\/pre>\n<p>  <\/p>\n<p>\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u043c \u043a\u043e\u043c\u0430\u043d\u0434\u0443 <code>yarn dev<\/code> \u0438\u043b\u0438 <code>npm run dev<\/code>.<\/p>\n<p>  <\/p>\n<p>\u042d\u0442\u043e \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u0437\u0430\u043f\u0443\u0441\u043a\u0443 3 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432 \u0434\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438:<\/p>\n<p>  <\/p>\n<ul>\n<li>\u0434\u043b\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 <code>http:\/\/localhost:3000<\/code>:<\/li>\n<\/ul>\n<p>  <img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/ve\/jd\/5q\/vejd5qrgzgbzll2pyb0jxpg0ucw.png\"\/>  <\/p>\n<p>  <\/p>\n<p>  <\/p>\n<ul>\n<li>\u0434\u043b\u044f \u0430\u0434\u043c\u0438\u043d\u043a\u0438 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 <code>http:\/\/localhost:4000<\/code>&#171;:<\/li>\n<\/ul>\n<p>  <img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/tz\/uy\/7-\/tzuy7-muwq_lxdant4hgpnfemjs.png\"\/>  <\/p>\n<p>  <\/p>\n<p>  <\/p>\n<ul>\n<li>\u0434\u043b\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 <code>http:\/\/localhost:5000<\/code>.<\/li>\n<\/ul>\n<p>  <\/p>\n<p>\u041c\u0435\u043d\u044f\u0435\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432 \u0430\u0434\u043c\u0438\u043d\u043a\u0435:<\/p>\n<p>  <img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/kp\/63\/wu\/kp63wu9r_cdnd-cyqwc9-p2dle0.png\"\/>  <\/p>\n<p>  <\/p>\n<p>  <\/p>\n<p>\u041f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u0430:<\/p>\n<p>  <img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/85\/fy\/cw\/85fycwhapup8y_auvgrldgwke1m.png\"\/>  <\/p>\n<p>  <\/p>\n<p>  <\/p>\n<p>\u0412\u0438\u0434\u0438\u043c, \u0447\u0442\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043b\u0438\u0441\u044c.<\/p>\n<p>  <\/p>\n<p>\u0420\u0430\u0431\u043e\u0442\u0430\u0435\u043c \u0441 \u0437\u0430\u0434\u0430\u0447\u0430\u043c\u0438:<\/p>\n<p>  <img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/sw\/yr\/nc\/swyrncbic8rxv1s0rxeczhg1bhk.png\"\/>  <\/p>\n<p>  <\/p>\n<p>  <\/p>\n<p>\u0417\u0430\u0434\u0430\u0447\u0438 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u044e\u0442\u0441\u044f\/\u043e\u0431\u043d\u043e\u0432\u043b\u044f\u044e\u0442\u0441\u044f\/\u0443\u0434\u0430\u043b\u044f\u044e\u0442\u0441\u044f \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u0432 \u0411\u0414.<\/p>\n<p>  <\/p>\n<p>\u041e\u0442\u043b\u0438\u0447\u043d\u043e. \u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u043a\u0430\u043a \u043e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f.<\/p>\n<p>  <\/p>\n<p>\u041f\u043e\u0436\u0430\u043b\u0443\u0439, \u044d\u0442\u043e \u0432\u0441\u0435, \u043e \u0447\u0435\u043c \u044f \u0445\u043e\u0442\u0435\u043b \u0432\u0430\u043c \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u0442\u044c \u0432 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438.<\/p>\n<p>  <\/p>\n<p>\u0411\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044e \u0437\u0430 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u0438 happy coding!<\/p>\n<p>  <\/p>\n<hr\/>\n<p>  <\/p>\n<p><a href=\"https:\/\/cloud.timeweb.com\/?utm_source=habr&amp;utm_medium=banner&amp;utm_campaign=cloud&amp;utm_content=direct&amp;utm_term=low\"><img decoding=\"async\" src=\"\/img\/image-loader.svg\" data-src=\"https:\/\/habrastorage.org\/webt\/bh\/0m\/fv\/bh0mfviu_rma7be5uylpewumuxi.png\"\/><\/a><\/p>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"v-portal\" style=\"display:none;\"><\/div>\n<\/div>\n<p> <!----> <!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/company\/timeweb\/blog\/650969\/\"> https:\/\/habr.com\/ru\/company\/timeweb\/blog\/650969\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body_version-1\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/webt\/b7\/dj\/ot\/b7djotv_rf_w4fh7wxkpeykxj6g.jpeg\" data-src=\"https:\/\/habrastorage.org\/webt\/b7\/dj\/ot\/b7djotv_rf_w4fh7wxkpeykxj6g.jpeg\" data-blurred=\"true\"\/>  <\/p>\n<p>  \u041f\u0440\u0438\u0432\u0435\u0442, \u0434\u0440\u0443\u0437\u044c\u044f! \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u044e \u0434\u0435\u043b\u0438\u0442\u044c\u0441\u044f \u0441 \u0432\u0430\u043c\u0438 \u0437\u0430\u043c\u0435\u0442\u043a\u0430\u043c\u0438 \u043e <a href=\"https:\/\/www.docker.com\/\"><code>Docker<\/code><\/a>.<\/p>\n<p>  <\/p>\n<p>\u0417\u0430\u043c\u0435\u0442\u043a\u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u0442 \u0438\u0437 4 \u0447\u0430\u0441\u0442\u0435\u0439: 2 \u0442\u0435\u043e\u0440\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0438 2 \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445. \u0415\u0441\u043b\u0438 \u0431\u044b\u0442\u044c \u0431\u043e\u043b\u0435\u0435 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u043c:<\/p>\n<p>  <\/p>\n<ul>\n<li><a href=\"https:\/\/habr.com\/ru\/company\/timeweb\/blog\/595687\/\">\u043f\u0435\u0440\u0432\u0430\u044f \u0447\u0430\u0441\u0442\u044c<\/a> \u043f\u043e\u0441\u0432\u044f\u0449\u0435\u043d\u0430 <code>Docker<\/code>, <code>Docker CLI<\/code> \u0438 <code>Dockerfile<\/code>;<\/li>\n<li>\u0432\u043e <a href=\"https:\/\/habr.com\/ru\/company\/timeweb\/blog\/597613\/\">\u0432\u0442\u043e\u0440\u043e\u0439 \u0447\u0430\u0441\u0442\u0438<\/a> \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043e <code>Docker Compose<\/code>.<\/li>\n<\/ul>\n<p>  <\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u043c\u044b \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u043c \u043f\u0440\u043e\u0441\u0442\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0441\u043e\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0438\u0437 \u0442\u0440\u0435\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432 \u0438 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445, \u0430 \u0432 \u0437\u0430\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u2014 &#171;\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0438\u0437\u0443\u0435\u043c&#187; \u0435\u0433\u043e.<\/p>\n<p>  <\/p>\n<p><a href=\"https:\/\/github.com\/harryheman\/docker-test\">\u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 \u0441 \u043a\u043e\u0434\u043e\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/a>.<\/p>\n<p>  <\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0430\u043c \u044d\u0442\u043e \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e, \u043f\u0440\u043e\u0448\u0443 \u043f\u043e\u0434 \u043a\u0430\u0442.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-329445","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/329445","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=329445"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/329445\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=329445"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=329445"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=329445"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}