{"data":{"post":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/","date":"2018-10-31"},"html":"<p>PS. If you’re familiar with Docker and don’t need a step by step guide, check only <a href=\"#multi-compose-files\">Multiple compose files</a>, <a href=\"#configure-it\">Configure it</a> and <a href=\"#magic\">Magic of multi-stage build</a>. Code is available on <a href=\"https://github.com/marcinlesek/docker-spa-setup\">GitHub</a>.</p>\n<h2>Table of contents</h2>\n<ul>\n<li>\n<p><a href=\"#intro\">Intro</a></p>\n<ul>\n<li><a href=\"#source-vs-artifact\">Source vs. Artifact</a></li>\n<li><a href=\"#docker\">Docker, wtf is that?</a></li>\n</ul>\n</li>\n<li>\n<p><a href=\"#own-playground\">Prepare your own playground</a></p>\n<ul>\n<li><a href=\"#setup-cra\">Setup create-react-app boiler template</a></li>\n<li><a href=\"#install-tools\">Install base tools - Docker &#x26; docker-compose</a></li>\n</ul>\n</li>\n<li>\n<p><a href=\"#dev-env\">Start development environment</a></p>\n<ul>\n<li><a href=\"#dockerfile-on-dev\">Dockerfile on development env? Oh please no…</a></li>\n<li><a href=\"#multi-compose-files\">Multiple compose files</a></li>\n<li><a href=\"#default-env-vars\">Default environment variables</a></li>\n<li><a href=\"#rocket-launch\">Rocket launch!</a></li>\n</ul>\n</li>\n<li>\n<p><a href=\"#prod-env\">Production time</a></p>\n<ul>\n<li><a href=\"#configure-it\">Configure it</a></li>\n<li><a href=\"#magic\">Magic of multi-stage build</a></li>\n<li><a href=\"#enter-stage\">Enter the stage!</a></li>\n</ul>\n</li>\n<li><a href=\"#extras\">Extras</a></li>\n</ul>\n<h2><a name=\"intro\"></a>Intro</h2>\n<p>Nowadays, frontend applications have a lot of dependencies (everyone knows these funny memes about the size of <code class=\"language-text\">node_modules</code> directory) and ways to be successfully started, deployed and built, so it’s not a piece of cake.\nI think everyone started with client apps where we’re editing files live on production and probably everything was in one file, or if not, the whole files were available from the client side.\nFortunately, those times is gone, for most of us. :)</p>\n<h3><a name=\"source-vs-artifact\"></a>Source vs. Artifact</h3>\n<p>Due to optimization, performance etc., actual standards are to parse, minify and combine our production code.\nIn this article, we will focus on setting up <strong>development</strong> and <strong>production</strong> environment for our app, but this setup will allow you to prepare more environments <strong>in less than 3 minutes</strong>.\nSo what is the difference between <code class=\"language-text\">source</code> and <code class=\"language-text\">artifact</code> in the SPA application?\nTake a look for this simple React app structure:</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">  .\n  ├── /build/                     # The folder for compiled production ready output\n  ├── /node_modules/              # 3rd-party libraries and utilities\n  ├── /src/                       # The source code of the application\n  │   ├── /components/            \n  │   ├── /routes/                \n  │   ├── /styles/                \n  │   ├── /...                    \n  │   └── ...                     \n  ├── /...\n  ├── package.json                # Project info like dependencies, authors, scripts etc.                           \n  └── ...                          </code></pre></div>\n<p>The whole <code class=\"language-text\">/src</code> folder is our app’s heart - main code that we’re developing. Running applications locally, on development environment by some <code class=\"language-text\">webpack-dev-server</code> or another serving tool, gives us an opportunity to have <em>hot module reload</em>, <em>live preview</em>, <em>fast run</em> etc.\nIt’s really good for development purpose and we’ll use it!</p>\n<h3><a name=\"docker\"></a>Docker, wtf is that?</h3>\n<p>I know, you probably know what is Docker and how it works, but if not, let me show you the short quote from <a href=\"https://www.docker.com/resources/what-container\">What is a Container</a> which is a great definition of Docker base block.</p>\n<blockquote>\n<p>A container is a standard unit of software that <strong>packages up code and all its dependencies</strong> so the application runs <strong>quickly and reliably from one computing environment to another</strong>. A Docker container image is a <strong>lightweight, standalone, executable package</strong> of software that includes everything needed to run an application: code, runtime, system tools, system libraries and settings.</p>\n</blockquote>\n<p>I have highlighted key information which will help you understand how it works and how huge benefits it brings to our lives are.\nLet’s tightly pack our application with all dependencies into a light-weight block called <strong>container</strong> to run it quickly on every environment.</p>\n<blockquote>\n<p>Available for both Linux and Windows-based applications, the containerized software will <strong>always run the same, regardless of the infrastructure</strong>. Containers <strong>isolate software from its environment and ensure that it works uniformly despite differences for instance</strong> between development and staging.</p>\n</blockquote>\n<p><img src=\"/img/what-is-container.png\" alt=\"Architecture template image\"></p>\n<p>Always the same result, bypassing the infrastructure.\nIt is a result of the way Docker works, shown above on architecture scheme.\nFor more details, check <a href=\"https://www.docker.com/resources/what-container\">What is a Container</a>.</p>\n<h2><a name=\"own-playground\"></a>Prepare your own playground</h2>\n<p>We’ll start with setting up our development environment based on <strong>Docker</strong> &#x26; <strong>docker-compose</strong> and <code class=\"language-text\">create-react-app</code> template.\nFirstly, we need to set up our basic app. Of course in the bigger project, we’ll have many more dependencies, services and complexity but the problem solution is almost the same.</p>\n<h3><a name=\"setup-cra\"></a>Setup create-react-app boiler template</h3>\n<p>Create our boiler template app:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">$ yarn create react-app docker-spa-setup</code></pre></div>\n<p>After that, we should see a message like this:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">Success<span class=\"token operator\">!</span> Created docker-spa-setup at /path/to/docker-spa-setup</code></pre></div>\n<p>Now, let’s run our hello world application:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">$ <span class=\"token function\">cd</span> docker-spa-setup\n$ yarn start</code></pre></div>\n<p>After a few seconds, we should see information regarding successfully compiled and served under <code class=\"language-text\">localhost:3000</code>, with all things we need to easily develop an app.\nGreat! </p>\n<h3><a name=\"install-tools\"></a>Install base tools - Docker &#x26; docker-compose</h3>\n<p>Now the real fun begins.\nNext things that we’ll need will be <code class=\"language-text\">docker</code> and <code class=\"language-text\">docker-compose</code>.\nThis tutorial was written using:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">$ docker -v\nDocker version 18.06.0-ce, build 0ffa825\n$ docker-compose -v\ndocker-compose version 1.21.0, build 5920eb0</code></pre></div>\n<p>If you don’t have <code class=\"language-text\">docker</code> or <code class=\"language-text\">docker-compose</code> on your local machine, check <a href=\"https://docs.docker.com/install/\">Docker installation guide</a> or <a href=\"https://docs.docker.com/compose/install/\">docker-compose installation guide</a>.\nDue to these versions, we could use <strong>compose files</strong> over <code class=\"language-text\">3.6</code> version.\nNow, in project root directory let’s create a directory called <code class=\"language-text\">.docker</code> where we’ll keep the whole Docker configuration.</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">$ <span class=\"token function\">mkdir</span> .docker\n$ <span class=\"token function\">cd</span> .docker</code></pre></div>\n<h2><a name=\"dev-env\"></a>Start development environment</h2>\n<p>Okay, we’ve set up our own local machine, so we have all the tools we need to start our journey.\nIf you know Docker a bit, your first association may be <strong>Dockerfile</strong>!\nOf course, you’re right, but let me explain about it a bit more… </p>\n<h3><a name=\"dockerfile-on-dev\"></a>Dockerfile on development env? Oh please no…</h3>\n<p><img src=\"https://pics.me.me/no-god-please-21245703.png\" alt=\"Oh god, please no!\"></p>\n<p>Some of you will probably not agree with me, but I will try to convince you, that <strong>using Dockerfile on development environment isn’t the best way</strong> you could choose (if not necessary, of course) - sometimes it’s a must have, but let’s say it officially: in most cases, <strong>it’s not</strong>.</p>\n<p>Dockerfile allows us to create a custom Docker image for our container from an existing one and add our changes to the build process.\nIt’s great, even we’ll be using it here in next chapters, but <strong>in production setup</strong>, where <em>optimization, security and light-weight</em> of the app is <strong>crucial</strong>. </p>\n<p>On the other hand, we have <strong>docker-compose</strong>, which could manage services (containers) with <code class=\"language-text\">docker-compose.yml</code> file.\nI would even say that it helps us to simplify the definition of our services by defining all containers in one file.\nAdditionally, it allows us to set ports, image to use, volumes, environments variables and many more! After two years with Docker in commercial products, I could say, for basic or mid-advanced projects, we could handle it with only <a href=\"https://docs.docker.com/compose/\">docker-compose</a>.\nBenefits?\n<strong>Less time under building application</strong> (mostly crucial for frontend developers working with watchers and hot reloads) because we’ll be reducing our build time without any of our custom commands for build own image.</p>\n<p>Be lighter and flexible, especially on the development environment, when you need this a lot with continuous changes you’re making.</p>\n<h3><a name=\"multi-compose-files\"></a>Multiple compose files</h3>\n<p>Firstly, what is <code class=\"language-text\">multiple compose files</code>?\nBy default, our docker-compose will read base <code class=\"language-text\">docker-compose.yml</code> file which according to the convention, it should be our base configuration, which <strong>will be shared over all environments</strong>.\nAfter that, compose will append the final docker-compose file with override file like <code class=\"language-text\">docker-compose.override.yml</code>.\nWhen some services are defined in both, they will be merged together.\nThis feature is really cool and will help us to keep our setup clear and easy to maintain.</p>\n<p>So, we’ll create <code class=\"language-text\">docker-compose.yml</code> which will be our base for all environments, also production etc. </p>\n<div class=\"gatsby-highlight\" data-language=\"yml\"><pre class=\"language-yml\"><code class=\"language-yml\"># docker-compose.yml\nversion: &#39;3.6&#39;\n\nservices:\n  client:\n    env_file:\n      - .env</code></pre></div>\n<p>So, what happened here? Probably you know <code class=\"language-text\">docker-compose</code> schema and properties, but if not, I will explain a little:</p>\n<ul>\n<li><code class=\"language-text\">version: 3.6</code> - Shows supported compose file version to <code class=\"language-text\">docker-compose</code>, so we’ll know what syntax and features we could use.</li>\n<li><code class=\"language-text\">services:</code> - main compose purpose, registering services to run them all neither using a command for one by one like <code class=\"language-text\">docker run ...</code></li>\n<li><code class=\"language-text\">client:</code> - registering our first and main service - <code class=\"language-text\">client</code> which will be our React app,</li>\n<li><code class=\"language-text\">env_file:</code> - pointing the <code class=\"language-text\">.env</code> file which should be used to get global environments values for our setup. This file helps us to storage variables for our app like exposed port, application environment, compose project name and much more, that we could easily change due to fact that they are in one place.</li>\n</ul>\n<p>Okay, now our <strong>Docker</strong> setup contains only base <code class=\"language-text\">docker-compose.yml</code>, so we need to add first override compose file for development environment: <code class=\"language-text\">docker-compose.dev.yml</code></p>\n<div class=\"gatsby-highlight\" data-language=\"yml\"><pre class=\"language-yml\"><code class=\"language-yml\"># docker-compose.dev.yml\nversion: &#39;3.6&#39;\n\nservices:\n  client:\n    image: node:carbon\n    volumes:\n      - ..:/opt/app:cached\n    ports:\n      - &quot;${NODE_PORT}:${NODE_PORT}&quot;\n    working_dir: /opt/app\n    command: &quot;/bin/sh -c &#39;yarn install --frozen-lockfile --production=false; yarn start&#39;&quot;</code></pre></div>\n<p>Describe a bit more this little magic:</p>\n<ul>\n<li><code class=\"language-text\">image:</code> - here we’re specifying that we want to use an image with predefined <code class=\"language-text\">node.js</code> in <code class=\"language-text\">carbon</code> (<code class=\"language-text\">8.x</code>) version. Based on this image, Docker will build a container for our application (<code class=\"language-text\">client</code>),</li>\n<li><code class=\"language-text\">volumes:</code> - this allows us to mount some data volume (we don’t need to copy, because we mount data from our host to container) as <code class=\"language-text\">HOST:CONTAINER</code> path. So we’re mounting <code class=\"language-text\">..</code> from the host (project root directory) to <code class=\"language-text\">/opt/app</code> in our container. We also add <code class=\"language-text\">:cached</code> to improve Mac’s performance. More details about it <a href=\"https://docs.docker.com/docker-for-mac/osxfs-caching/#cached\">here</a>.</li>\n<li><code class=\"language-text\">ports:</code> - here we assign port from our host to port in our container (<code class=\"language-text\">HOST:CONTAINER</code>), so we could access our container from outside e.g. <code class=\"language-text\">5000:5000</code> maps port <code class=\"language-text\">5000</code> from host to port <code class=\"language-text\">5000</code> in the container. We could also parametrize these values so we’re getting them from env variables,</li>\n<li><code class=\"language-text\">working_dir:</code> - specifies root app directory, where <code class=\"language-text\">docker-compose</code> will be working,</li>\n<li><code class=\"language-text\">command:</code> - command which will run our container. We’re passing to shell our commands (via <code class=\"language-text\">/bin/sh -c</code>), which firstly installs dependencies with frozen lockfile (don’t generate new <code class=\"language-text\">yarn.lockfile</code> and download the same versions) and forces production flag to false (be sure, that <code class=\"language-text\">devDependencies</code> also will be installed).</li>\n</ul>\n<h3><a name=\"default-env-vars\"></a>Default environment variables</h3>\n<p>For now, our Docker setup won’t do anything useful for us.\nAlso, if you’ll try now to run it, compose will warn you about lack of image or build source.\nFirstly, let’s add <code class=\"language-text\">.env</code> template file called <code class=\"language-text\">.env.dev.dist</code>!</p>\n<div class=\"gatsby-highlight\" data-language=\"apacheconf\"><pre class=\"language-apacheconf\"><code class=\"language-apacheconf\"><span class=\"token comment\"># .env.dev.dist</span>\nCOMPOSE_PROJECT_NAME=docker-spa-setup\nCOMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml\nNODE_ENV=development\nNODE_PORT=3000</code></pre></div>\n<p>We added few environment variables:</p>\n<ul>\n<li><code class=\"language-text\">COMPOSE_PROJECT_NAME</code> - set our project name, it will be used as a prefix for creating our services containers later, so it should be short and intuitive,</li>\n<li><code class=\"language-text\">COMPOSE_FILE</code> - this one is due to the fact, that we want to use multiple compose files. We specify which files should be overridden and by which one. By this variable, we could setup it per application environment, but about this will be later. Syntax looks like: <code class=\"language-text\">base:override1:override2</code> etc.,</li>\n<li><code class=\"language-text\">NODE_ENV</code> will be used in our app to specify in which environment we’re now, like development or production,</li>\n<li><code class=\"language-text\">NODE_PORT</code> to easily change the exposed port from our service container, now we set it as default <code class=\"language-text\">3000</code>,</li>\n</ul>\n<p>Our template is ready, we need to copy it to <em>proper</em> <code class=\"language-text\">.env</code> file:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">$ <span class=\"token function\">cp</span> .env.dev.dist .env</code></pre></div>\n<h3><a name=\"rocket-launch\"></a>Rocket launch!</h3>\n<p>Yeah! Now with this <code class=\"language-text\">docker-compose.dev.yml</code> and the <code class=\"language-text\">.env</code> file we’re able to start our app in Docker:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">$ docker-compose up -d\nStarting docker-spa-setup_client_1 <span class=\"token punctuation\">..</span>. <span class=\"token keyword\">done</span></code></pre></div>\n<p><strong>Congratulations</strong>! You <strong>set up your development</strong> environment.</p>\n<h2><a name=\"prod-env\"></a>Production time</h2>\n<p>Now we can assume that few sprints of development have passed and you have your dream <em>MVP ready to production</em>. But only development environment is working now - it’s heavy, not optimized and with a lot of unneeded logs.\nLet’s change it!</p>\n<h3><a name=\"configure-it\"></a>Configure it</h3>\n<p>Early on, we should add new <code class=\"language-text\">docker-compose</code> override for your production build, let’s call it <code class=\"language-text\">docker-compose.prod.yml</code>:</p>\n<div class=\"gatsby-highlight\" data-language=\"yml\"><pre class=\"language-yml\"><code class=\"language-yml\"># docker-compose.prod.yml\nversion: &#39;3.6&#39;\n\nservices:\n  client:\n    build:\n      context: ../\n      dockerfile: .docker/Dockerfile\n    ports:\n      - ${NGINX_PORT}:80\n    restart: unless-stopped</code></pre></div>\n<p>As you can see, it’s changed a lot from <code class=\"language-text\">dev</code> version, so nothing could be passed up to our base <code class=\"language-text\">docker-compose.yml</code> file.\nNow we’ll be using the build process rather than a ready-made image from Docker Hub and <a href=\"http://nginx.org/\">nginx</a> to serve our app.\nOf course, you could also use <a href=\"https://httpd.apache.org/\">Apache</a> or another HTTP server.\nOur service is called <code class=\"language-text\">client</code>, just as in <code class=\"language-text\">dev</code> setup, because we’re building this same app in another configuration, we won’t break our network connection (for example <a href=\"https://docs.docker.com/docker-cloud/apps/service-links/#using-service-and-container-names-as-hostnames\">using service name rather than IPs</a>).\nLet’s take a closer look at it:</p>\n<ul>\n<li>\n<p><code class=\"language-text\">build:</code> - here we’re specifying our build process, how we would create our image for our production containers,</p>\n<ul>\n<li><code class=\"language-text\">context:</code> - we’re passing, that context for your build is the root application directory (information for Docker daemon), so we’re going one level up. By default, Docker will be looking for Dockerfile here, but we’ll specify it below.</li>\n<li><code class=\"language-text\">dockerfile:</code> - direct path to your Dockerfile,</li>\n</ul>\n</li>\n<li><code class=\"language-text\">ports</code> - same as above, but now with another environment variable <code class=\"language-text\">NGINX_PORT</code> which we’ll set up later,</li>\n<li><code class=\"language-text\">restart:</code> - container restart policy. We don’t want our production to be down for a long time due to some mistakes or not to start after server restart - <code class=\"language-text\">unless-stopped</code> will cover it for us, containers will be restarted always, except for the moment when we’ll stop them manually (e.g. <code class=\"language-text\">docker-compose stop</code>). I don’t recommend it on our dev machines, because it’ll start always with our operating system and sometimes you don’t want to code, so free up your resources.</li>\n</ul>\n<p>This basic <code class=\"language-text\">docker-compose.prod.yml</code> <strong>won’t handle everything, but it’s really good to start for production setup</strong> (or good skeleton for it).\nFor example, here we didn’t handle HTTPS connection on 443 port (but it’s easy to add, don’t worry #trueStory).</p>\n<p>Now let’s build our <code class=\"language-text\">.env</code> file for production.\nBut wait, we already have a file named <code class=\"language-text\">.env</code> - and <strong>it’s wrong</strong>!\nFor many reasons, as an example security, you shouldn’t keep <code class=\"language-text\">.env</code> files in the repository.\nIt’s really easy to change them between deploys without changing any code and also it’s very often to store in them some keys that couldn’t be public.\nThey should show us what it’s necessary for the application to work on a particular environment, but it should be completed manually (<strong>especially for private keys!</strong>).\nTo be sure we didn’t do it, we’ll add a record to <code class=\"language-text\">.gitginore</code>:</p>\n<div class=\"gatsby-highlight\" data-language=\"yml\"><pre class=\"language-yml\"><code class=\"language-yml\"># .gitignore\n...\n\n# docker\n.docker/.env</code></pre></div>\n<p>Now let’s rename our <code class=\"language-text\">.env</code> to <code class=\"language-text\">.env.dev.dist</code> where we’ll store our development variables setup and let’s create <code class=\"language-text\">.env.prod.dist</code> for production.\nWith this move, whenever you’ll be changing the environment or setting it for the first time, you will have to do <code class=\"language-text\">cp .env.{ENV}.dist .env</code> correctly.\nNothing special, but this must be kept in mind.</p>\n<div class=\"gatsby-highlight\" data-language=\"apacheconf\"><pre class=\"language-apacheconf\"><code class=\"language-apacheconf\"><span class=\"token comment\"># .env.prod.dist</span>\nCOMPOSE_PROJECT_NAME=docker-spa-setup\nCOMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml\nNODE_ENV=production\nNGINX_PORT=80</code></pre></div>\n<p>We’ve three differences between our <code class=\"language-text\">.env.dev.dist</code> file, let’s describe them:</p>\n<ul>\n<li>we changed our override <code class=\"language-text\">docker-compose</code> file for <code class=\"language-text\">prod</code> version,</li>\n<li><code class=\"language-text\">NODE_ENV</code> is set for <code class=\"language-text\">production</code>, it’s necessary, because many JavaScript engines, frameworks and libraries check this variable and optimize themselves according to its value,</li>\n<li><code class=\"language-text\">NGINX_PORT</code> - new variable instead of <code class=\"language-text\">NODE_PORT</code>, used to specify on which port we want had our production app served from the container by <strong>nginx</strong>.</li>\n</ul>\n<p>Nothing hard, isn’t it?</p>\n<h3><a name=\"magic\"></a>Magic of multi-stage build</h3>\n<p>This is the moment everyone was waiting for - we can create <code class=\"language-text\">Dockerfile</code> with <a href=\"https://docs.docker.com/develop/develop-images/multistage-build/\">mutli stage build</a>!</p>\n<div class=\"gatsby-highlight\" data-language=\"dockerfile\"><pre class=\"language-dockerfile\"><code class=\"language-dockerfile\"># Dockerfile\n# Stage 1 - the build process\nFROM node:carbon-alpine as builder\nRUN mkdir /opt/app\nWORKDIR /opt/app\nCOPY . ./\nRUN yarn; yarn build\n\n# Stage 2 - the production environment\nFROM nginx:alpine\n\nCOPY .docker/nginx/default.conf /etc/nginx/conf.d/\nRUN rm -rf /usr/share/nginx/html/*\nCOPY --from=builder /opt/app/build /usr/share/nginx/html\n\nCMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</code></pre></div>\n<p>Wow, what happened here?\nLet’s look on <strong>first stage</strong>, where we’ll <strong>build</strong> our app. </p>\n<ol>\n<li>Firstly, we pull <code class=\"language-text\">node:carbon-alpine</code> image (alpine is a lightweight image without any not necessary tools) and name it <code class=\"language-text\">builder</code> in our Dockerfile space.</li>\n<li>In the second line, we’re creating directory <code class=\"language-text\">/opt/app</code> where we’ll have our root project directory.</li>\n<li>Tell Docker, that this folder (<code class=\"language-text\">/opt/app</code>) in a container is root, where we’ll be executing everything later (with this we don’t have to use absolute paths).</li>\n<li>Now we’re copying the whole project here.</li>\n<li>Run dependency install and production build to create our app. Of course, you could also use npm: <code class=\"language-text\">npm i; npm run build</code>.</li>\n</ol>\n<p>Probably you’re thinking now - okay, we had our container, our app is built, why not add the second service as HTTP server in <code class=\"language-text\">docker-compose.prod.yml</code> file and serve this app?\nHere is a place, <strong>where multi-stage build enters</strong>!\nOf course, you’re right, but in this container, you’d the whole app source, dependencies (<code class=\"language-text\">node_modules</code> bigger than the sun) and nothing to serve it (right now).\nThe multi-stage could make it thinner, with nginx and only with one service!\nSounds cool?\nLet’s check what is going on in the second build stage.</p>\n<ol start=\"6\">\n<li>Now, let’s pull <code class=\"language-text\">nginx:alpine</code> image (lightweight HTTP server) which will be serving our app (and in future could be also reverse proxy, load balancer etc.).</li>\n<li>After that, we need to set it up. Luckily, it could be only copying default configuration prepared earlier (it will be displayed below, due to fact configuring nginx could be placed for another series of posts, we’ll leave it for now).</li>\n<li>Remove everything from default nginx serve directory (be sure we have clean space for our application).</li>\n<li><strong>Magic is here!</strong> Now we’re copying only our build output from <code class=\"language-text\">builder</code> container (our <code class=\"language-text\">node</code> image where the application was built) to our new container with nginx to default serve location. After this move, Docker <strong>will automatically remove old, not used builder container</strong> and we’ll have <code class=\"language-text\">nginx:alpine</code> container with <strong>only built application files</strong> as <code class=\"language-text\">client</code> service. <strong>Amazing</strong>!</li>\n<li>A command which will be used to run container, here we’re starting nginx with flag <code class=\"language-text\">daemon off</code> so he will be run in the background and serve our application.</li>\n</ol>\n<p>Almost done!</p>\n<h3><a name=\"enter-stage\"></a>Enter the stage!</h3>\n<p>The last thing to do, create space for nginx related properties/files (it’s a good way to store files &#x26; configurations with their purpose, also it’s easier to manage it in bigger projects):</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">$ <span class=\"token function\">mkdir</span> .docker/nginx</code></pre></div>\n<p>And create <code class=\"language-text\">default.conf</code> file to set up our server:</p>\n<div class=\"gatsby-highlight\" data-language=\"apacheconf\"><pre class=\"language-apacheconf\"><code class=\"language-apacheconf\"><span class=\"token comment\"># default.conf</span>\nserver {\n  <span class=\"token directive-inline property\">listen</span> 80;\n\n  sendfile on;\n  default_type application/octet-stream;\n\n  server_name your-site.com;\n\n  gzip on;\n  gzip_http_version 1.1;\n  gzip_disable      <span class=\"token string\">\"MSIE [1-6]\\.\"</span>;\n  gzip_min_length   256;\n  gzip_vary         on;\n  gzip_proxied      expired no-cache no-store private auth;\n  gzip_types        text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;\n  gzip_comp_level   9;\n\n  root /usr/share/nginx/html;\n\n  location / {\n    try_files <span class=\"token variable\">$uri</span> <span class=\"token variable\">$uri</span>/ /index.html =404;\n  }\n}</code></pre></div>\n<p>This setup handles the only HTTP and serves our <code class=\"language-text\">index.html</code>.\nYou only need to change <code class=\"language-text\">server_name</code> property value to your own url address.\nAfter that, you only have to run it:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">$ docker-compose up -d</code></pre></div>\n<p>You’ll see Docker output regarding building our application with whole steps:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">$ docker-compose up -d\nBuilding client\nStep 1/10 <span class=\"token keyword\">:</span> FROM node:carbon-alpine as builder\n ---<span class=\"token operator\">></span> df48b68da02a\nStep 2/10 <span class=\"token keyword\">:</span> RUN <span class=\"token function\">mkdir</span> /opt/app\n\n<span class=\"token punctuation\">..</span>.\n\nSuccessfully built 0977583c3825\nSuccessfully tagged docker-spa-setup_client:latest\nStarting docker-spa-setup_client_1 <span class=\"token punctuation\">..</span>. <span class=\"token keyword\">done</span></code></pre></div>\n<p>Now, you could check your site locally entering <code class=\"language-text\">localhost:{NGINX_PORT}</code> and officially say <strong>hello my production app</strong>! Whoooa!</p>\n<h2><a name=\"extras\"></a>Extras</h2>\n<p>I hope this post will help you to understand how <strong>multi-stage build</strong> works and how you could use it to improve your SPA app (not that it won’t work perfectly well for API too, but it’s probably a topic for another post probably).\nYou could find a working code on my <a href=\"https://github.com/marcinlesek/docker-spa-setup\">GitHub</a>, so don’t hesitate to check it if you’ll force some troubles with this guide or ask me some questions here or on my <a href=\"https://twitter.com/marcinlesek\">Twitter</a>, I will try to help you. :)</p>\n<p>I also leave here some helpful links (some basics and some advanced a bit, used to create this article and also mentioned above), but to be honest, the best way to learn Docker, is diggin’ into it and checking their documentation, which is so great!</p>\n<ul>\n<li><a href=\"https://www.docker.com/resources/what-container\">What is a Container</a>,</li>\n<li><a href=\"https://docs.docker.com/get-started/\">Get started with Docker</a>,</li>\n<li><a href=\"https://docs.docker.com/compose/gettingstarted/\">Get started with Docker Compose</a>,</li>\n<li><a href=\"https://docs.docker.com/develop/develop-images/multistage-build/\">Multi stage build</a>,</li>\n<li><a href=\"https://docs.docker.com/compose/extends/#multiple-compose-files\">Multiple Compose files</a></li>\n</ul>\n<p>Good luck!</p>","frontmatter":{"title":"Dev & prod ready Docker setup for SPA app","subTitle":"Simple and light env setup to run SPA apps on various configurations with Docker multi-stage build","postAuthor":"Marcin Łesek (@marcinlesek)","cover":"/img/docker.png"}},"tags":{"group":[{"name":"$OSTYPE","totalCount":1,"edges":[{"node":{"fields":{"slug":"/cross-system-xdebug-docker-compose-setup/"}}}]},{"name":".env","totalCount":1,"edges":[{"node":{"fields":{"slug":"/cross-system-xdebug-docker-compose-setup/"}}}]},{"name":".nvrmc","totalCount":1,"edges":[{"node":{"fields":{"slug":"/travis-problematic-python-and-node/"}}}]},{"name":"AUTO_INCREMENT","totalCount":1,"edges":[{"node":{"fields":{"slug":"/is-mysql-autoincrement-really-monotonic/"}}}]},{"name":"Angular","totalCount":1,"edges":[{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"Command","totalCount":1,"edges":[{"node":{"fields":{"slug":"/complex-command-handler-in-javascript/"}}}]},{"name":"DQL","totalCount":2,"edges":[{"node":{"fields":{"slug":"/doctrine-new-dql-operator-and-objects/"}}},{"node":{"fields":{"slug":"/mysql-decimal-format/"}}}]},{"name":"Data Transfer Object","totalCount":1,"edges":[{"node":{"fields":{"slug":"/doctrine-new-dql-operator-and-objects/"}}}]},{"name":"Docker","totalCount":3,"edges":[{"node":{"fields":{"slug":"/cross-system-xdebug-docker-compose-setup/"}}},{"node":{"fields":{"slug":"/quick-import-of-mysql-database-dump/"}}},{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"Docker Compose","totalCount":3,"edges":[{"node":{"fields":{"slug":"/cross-system-xdebug-docker-compose-setup/"}}},{"node":{"fields":{"slug":"/quick-import-of-mysql-database-dump/"}}},{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"Doctrine","totalCount":2,"edges":[{"node":{"fields":{"slug":"/doctrine-new-dql-operator-and-objects/"}}},{"node":{"fields":{"slug":"/mysql-decimal-format/"}}}]},{"name":"Doctrine NEW","totalCount":1,"edges":[{"node":{"fields":{"slug":"/doctrine-new-dql-operator-and-objects/"}}}]},{"name":"ES7","totalCount":1,"edges":[{"node":{"fields":{"slug":"/async-await-with-express/"}}}]},{"name":"Elastica","totalCount":1,"edges":[{"node":{"fields":{"slug":"/elasticsearch-script-unknown-field-source-parser-not-found/"}}}]},{"name":"Elasticsearch","totalCount":1,"edges":[{"node":{"fields":{"slug":"/elasticsearch-script-unknown-field-source-parser-not-found/"}}}]},{"name":"Elasticsearch script","totalCount":1,"edges":[{"node":{"fields":{"slug":"/elasticsearch-script-unknown-field-source-parser-not-found/"}}}]},{"name":"Express","totalCount":1,"edges":[{"node":{"fields":{"slug":"/async-await-with-express/"}}}]},{"name":"Git","totalCount":2,"edges":[{"node":{"fields":{"slug":"/git-not-detecting-renames-quick-workarounds/"}}},{"node":{"fields":{"slug":"/git-reverting-branch-removal/"}}}]},{"name":"Git renamed","totalCount":1,"edges":[{"node":{"fields":{"slug":"/git-not-detecting-renames-quick-workarounds/"}}}]},{"name":"HTTP","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-does-slack-communicate-with-spotify/"}}}]},{"name":"JavaScript","totalCount":3,"edges":[{"node":{"fields":{"slug":"/async-await-with-express/"}}},{"node":{"fields":{"slug":"/complex-command-handler-in-javascript/"}}},{"node":{"fields":{"slug":"/how-to-implement-redux-in-react/"}}}]},{"name":"Kafka","totalCount":1,"edges":[{"node":{"fields":{"slug":"/is-mysql-autoincrement-really-monotonic/"}}}]},{"name":"MySQL","totalCount":4,"edges":[{"node":{"fields":{"slug":"/mysql-decimal-format/"}}},{"node":{"fields":{"slug":"/is-mysql-autoincrement-really-monotonic/"}}},{"node":{"fields":{"slug":"/php-with-mysql-8/"}}},{"node":{"fields":{"slug":"/quick-import-of-mysql-database-dump/"}}}]},{"name":"MySQL 8","totalCount":1,"edges":[{"node":{"fields":{"slug":"/php-with-mysql-8/"}}}]},{"name":"Node.js","totalCount":1,"edges":[{"node":{"fields":{"slug":"/travis-problematic-python-and-node/"}}}]},{"name":"ORM","totalCount":1,"edges":[{"node":{"fields":{"slug":"/doctrine-new-dql-operator-and-objects/"}}}]},{"name":"PDO","totalCount":1,"edges":[{"node":{"fields":{"slug":"/php-with-mysql-8/"}}}]},{"name":"PDO_MYSQL","totalCount":1,"edges":[{"node":{"fields":{"slug":"/php-with-mysql-8/"}}}]},{"name":"PHP","totalCount":6,"edges":[{"node":{"fields":{"slug":"/doctrine-new-dql-operator-and-objects/"}}},{"node":{"fields":{"slug":"/mysql-decimal-format/"}}},{"node":{"fields":{"slug":"/cross-system-xdebug-docker-compose-setup/"}}},{"node":{"fields":{"slug":"/php-with-mysql-8/"}}},{"node":{"fields":{"slug":"/elasticsearch-script-unknown-field-source-parser-not-found/"}}},{"node":{"fields":{"slug":"/how-to-write-a-phing-target-autocomplete-bash-script/"}}}]},{"name":"Phing","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-write-a-phing-target-autocomplete-bash-script/"}}}]},{"name":"Promise","totalCount":1,"edges":[{"node":{"fields":{"slug":"/complex-command-handler-in-javascript/"}}}]},{"name":"Python","totalCount":1,"edges":[{"node":{"fields":{"slug":"/travis-problematic-python-and-node/"}}}]},{"name":"React","totalCount":2,"edges":[{"node":{"fields":{"slug":"/how-to-implement-redux-in-react/"}}},{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"Redux","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-implement-redux-in-react/"}}}]},{"name":"Redux store","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-implement-redux-in-react/"}}}]},{"name":"SPA","totalCount":1,"edges":[{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"Slack","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-does-slack-communicate-with-spotify/"}}}]},{"name":"Spotify","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-does-slack-communicate-with-spotify/"}}}]},{"name":"Travis","totalCount":2,"edges":[{"node":{"fields":{"slug":"/travis-problematic-python-and-node/"}}},{"node":{"fields":{"slug":"/elasticsearch-script-unknown-field-source-parser-not-found/"}}}]},{"name":"Vue","totalCount":1,"edges":[{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"XDEBUG_CONFIG","totalCount":1,"edges":[{"node":{"fields":{"slug":"/cross-system-xdebug-docker-compose-setup/"}}}]},{"name":"XDebug","totalCount":1,"edges":[{"node":{"fields":{"slug":"/cross-system-xdebug-docker-compose-setup/"}}}]},{"name":"ai","totalCount":1,"edges":[{"node":{"fields":{"slug":"/weekly-ai-bites-last-manual-gap-qa-testing/"}}}]},{"name":"amazon","totalCount":1,"edges":[{"node":{"fields":{"slug":"/minio-as-s3-replacement-in-development-and-beyond/"}}}]},{"name":"apple-watch","totalCount":1,"edges":[{"node":{"fields":{"slug":"/aws-cognito-without-library/"}}}]},{"name":"asp.net core","totalCount":1,"edges":[{"node":{"fields":{"slug":"/net-in-my-browser/"}}}]},{"name":"async","totalCount":2,"edges":[{"node":{"fields":{"slug":"/async-await-with-express/"}}},{"node":{"fields":{"slug":"/complex-command-handler-in-javascript/"}}}]},{"name":"autocomplete","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-write-a-phing-target-autocomplete-bash-script/"}}}]},{"name":"await","totalCount":2,"edges":[{"node":{"fields":{"slug":"/async-await-with-express/"}}},{"node":{"fields":{"slug":"/complex-command-handler-in-javascript/"}}}]},{"name":"aws","totalCount":3,"edges":[{"node":{"fields":{"slug":"/minio-as-s3-replacement-in-development-and-beyond/"}}},{"node":{"fields":{"slug":"/time-out-of-sync-in-aws-ec2/"}}},{"node":{"fields":{"slug":"/aws-cognito-without-library/"}}}]},{"name":"aws.cli","totalCount":1,"edges":[{"node":{"fields":{"slug":"/travis-problematic-python-and-node/"}}}]},{"name":"bash","totalCount":2,"edges":[{"node":{"fields":{"slug":"/cross-system-xdebug-docker-compose-setup/"}}},{"node":{"fields":{"slug":"/how-to-write-a-phing-target-autocomplete-bash-script/"}}}]},{"name":"bitbucket","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-quickly-remove-merged-remote-branches/"}}}]},{"name":"blazor","totalCount":1,"edges":[{"node":{"fields":{"slug":"/net-in-my-browser/"}}}]},{"name":"build","totalCount":1,"edges":[{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"builder pattern","totalCount":1,"edges":[{"node":{"fields":{"slug":"/make-jaxb-great-again/"}}}]},{"name":"c#","totalCount":1,"edges":[{"node":{"fields":{"slug":"/net-in-my-browser/"}}}]},{"name":"ci","totalCount":1,"edges":[{"node":{"fields":{"slug":"/unresolved-check-from-travis-on-github-pull-request/"}}}]},{"name":"claude-code","totalCount":1,"edges":[{"node":{"fields":{"slug":"/weekly-ai-bites-last-manual-gap-qa-testing/"}}}]},{"name":"cognito","totalCount":1,"edges":[{"node":{"fields":{"slug":"/aws-cognito-without-library/"}}}]},{"name":"colors","totalCount":1,"edges":[{"node":{"fields":{"slug":"/naming-sass-color-variables/"}}}]},{"name":"continuous integration","totalCount":1,"edges":[{"node":{"fields":{"slug":"/unresolved-check-from-travis-on-github-pull-request/"}}}]},{"name":"decimal","totalCount":1,"edges":[{"node":{"fields":{"slug":"/mysql-decimal-format/"}}}]},{"name":"docker","totalCount":1,"edges":[{"node":{"fields":{"slug":"/minio-as-s3-replacement-in-development-and-beyond/"}}}]},{"name":"ec2","totalCount":1,"edges":[{"node":{"fields":{"slug":"/time-out-of-sync-in-aws-ec2/"}}}]},{"name":"environment","totalCount":1,"edges":[{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"equals","totalCount":1,"edges":[{"node":{"fields":{"slug":"/jpa-and-uuid/"}}}]},{"name":"flysystem","totalCount":1,"edges":[{"node":{"fields":{"slug":"/minio-as-s3-replacement-in-development-and-beyond/"}}}]},{"name":"front-end","totalCount":1,"edges":[{"node":{"fields":{"slug":"/net-in-my-browser/"}}}]},{"name":"git","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-quickly-remove-merged-remote-branches/"}}}]},{"name":"git commit","totalCount":1,"edges":[{"node":{"fields":{"slug":"/git-not-detecting-renames-quick-workarounds/"}}}]},{"name":"git reflog","totalCount":1,"edges":[{"node":{"fields":{"slug":"/git-reverting-branch-removal/"}}}]},{"name":"git status","totalCount":1,"edges":[{"node":{"fields":{"slug":"/git-not-detecting-renames-quick-workarounds/"}}}]},{"name":"github","totalCount":3,"edges":[{"node":{"fields":{"slug":"/pull-request-templates-on-github/"}}},{"node":{"fields":{"slug":"/unresolved-check-from-travis-on-github-pull-request/"}}},{"node":{"fields":{"slug":"/how-to-quickly-remove-merged-remote-branches/"}}}]},{"name":"guide","totalCount":1,"edges":[{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"hashcode","totalCount":1,"edges":[{"node":{"fields":{"slug":"/jpa-and-uuid/"}}}]},{"name":"hibernate","totalCount":1,"edges":[{"node":{"fields":{"slug":"/jpa-and-uuid/"}}}]},{"name":"java","totalCount":2,"edges":[{"node":{"fields":{"slug":"/make-jaxb-great-again/"}}},{"node":{"fields":{"slug":"/jpa-and-uuid/"}}}]},{"name":"java.time","totalCount":1,"edges":[{"node":{"fields":{"slug":"/make-jaxb-great-again/"}}}]},{"name":"jaxb","totalCount":1,"edges":[{"node":{"fields":{"slug":"/make-jaxb-great-again/"}}}]},{"name":"jdk8","totalCount":1,"edges":[{"node":{"fields":{"slug":"/make-jaxb-great-again/"}}}]},{"name":"jpa","totalCount":1,"edges":[{"node":{"fields":{"slug":"/jpa-and-uuid/"}}}]},{"name":"maven","totalCount":1,"edges":[{"node":{"fields":{"slug":"/make-jaxb-great-again/"}}}]},{"name":"middleware","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-implement-redux-in-react/"}}}]},{"name":"mono","totalCount":1,"edges":[{"node":{"fields":{"slug":"/net-in-my-browser/"}}}]},{"name":"multi-stage","totalCount":1,"edges":[{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"naming","totalCount":1,"edges":[{"node":{"fields":{"slug":"/naming-sass-color-variables/"}}}]},{"name":"networking","totalCount":1,"edges":[{"node":{"fields":{"slug":"/time-out-of-sync-in-aws-ec2/"}}}]},{"name":"ntp","totalCount":1,"edges":[{"node":{"fields":{"slug":"/time-out-of-sync-in-aws-ec2/"}}}]},{"name":"nvm","totalCount":1,"edges":[{"node":{"fields":{"slug":"/travis-problematic-python-and-node/"}}}]},{"name":"persistence","totalCount":1,"edges":[{"node":{"fields":{"slug":"/jpa-and-uuid/"}}}]},{"name":"port 4381","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-does-slack-communicate-with-spotify/"}}}]},{"name":"pr","totalCount":1,"edges":[{"node":{"fields":{"slug":"/pull-request-templates-on-github/"}}}]},{"name":"production","totalCount":1,"edges":[{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"project","totalCount":1,"edges":[{"node":{"fields":{"slug":"/pull-request-templates-on-github/"}}}]},{"name":"pull","totalCount":1,"edges":[{"node":{"fields":{"slug":"/pull-request-templates-on-github/"}}}]},{"name":"qa","totalCount":1,"edges":[{"node":{"fields":{"slug":"/weekly-ai-bites-last-manual-gap-qa-testing/"}}}]},{"name":"razor","totalCount":1,"edges":[{"node":{"fields":{"slug":"/net-in-my-browser/"}}}]},{"name":"reducer","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-implement-redux-in-react/"}}}]},{"name":"regex","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-write-a-phing-target-autocomplete-bash-script/"}}}]},{"name":"repo","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-quickly-remove-merged-remote-branches/"}}}]},{"name":"repository","totalCount":1,"edges":[{"node":{"fields":{"slug":"/how-to-quickly-remove-merged-remote-branches/"}}}]},{"name":"s3","totalCount":1,"edges":[{"node":{"fields":{"slug":"/minio-as-s3-replacement-in-development-and-beyond/"}}}]},{"name":"sass","totalCount":1,"edges":[{"node":{"fields":{"slug":"/naming-sass-color-variables/"}}}]},{"name":"setup","totalCount":1,"edges":[{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"skills","totalCount":1,"edges":[{"node":{"fields":{"slug":"/weekly-ai-bites-last-manual-gap-qa-testing/"}}}]},{"name":"styles","totalCount":1,"edges":[{"node":{"fields":{"slug":"/naming-sass-color-variables/"}}}]},{"name":"symfony","totalCount":1,"edges":[{"node":{"fields":{"slug":"/minio-as-s3-replacement-in-development-and-beyond/"}}}]},{"name":"templates","totalCount":1,"edges":[{"node":{"fields":{"slug":"/pull-request-templates-on-github/"}}}]},{"name":"test-automation","totalCount":1,"edges":[{"node":{"fields":{"slug":"/weekly-ai-bites-last-manual-gap-qa-testing/"}}}]},{"name":"transaction","totalCount":1,"edges":[{"node":{"fields":{"slug":"/is-mysql-autoincrement-really-monotonic/"}}}]},{"name":"travis","totalCount":1,"edges":[{"node":{"fields":{"slug":"/unresolved-check-from-travis-on-github-pull-request/"}}}]},{"name":"tutorial","totalCount":2,"edges":[{"node":{"fields":{"slug":"/how-to-implement-redux-in-react/"}}},{"node":{"fields":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}}]},{"name":"ubuntu","totalCount":1,"edges":[{"node":{"fields":{"slug":"/time-out-of-sync-in-aws-ec2/"}}}]},{"name":"unsupported","totalCount":1,"edges":[{"node":{"fields":{"slug":"/aws-cognito-without-library/"}}}]},{"name":"uuid","totalCount":1,"edges":[{"node":{"fields":{"slug":"/jpa-and-uuid/"}}}]},{"name":"variables","totalCount":1,"edges":[{"node":{"fields":{"slug":"/naming-sass-color-variables/"}}}]},{"name":"wasm","totalCount":1,"edges":[{"node":{"fields":{"slug":"/net-in-my-browser/"}}}]},{"name":"watchos","totalCount":1,"edges":[{"node":{"fields":{"slug":"/aws-cognito-without-library/"}}}]},{"name":"web","totalCount":1,"edges":[{"node":{"fields":{"slug":"/net-in-my-browser/"}}}]}]},"author":{"id":"37bda159-ff45-50c9-aeba-18326b7ca66b","childMarkdownRemark":{"html":"<p><strong>XSolve</strong> We are Agile Software House focused on PHP/Symfony, #JavaScript, #Java and #Mobile (iOS, Android, Windows Phone) #Inc5000 European company in 2018.\n<br>\n<a href=\"https://xsolve.software\">xsolve.software</a></p>"}},"footnote":{"id":"b4474c9b-7be6-5f34-866a-cdfffa885bce","childMarkdownRemark":{"html":"<ul>\n<li>From <a href=\"https://www.boldare.com/\">Boldare</a></li>\n<li>with <a href=\"https://github.com/greglobinski/gatsby-starter-personal-blog/\">Gatsby starter</a></li>\n<li>delivered by <a href=\"https://www.netlify.com/\">Netlify</a></li>\n</ul>"}},"site":{"siteMetadata":{"facebook":{"appId":""}}}},"pageContext":{"slug":"/Dev-and-prod-ready-Docker-setup-for-SPA-app/"}}