RSSAmplifier

Blog on dlford.io · Jan 24, 2020

Cloud Native Development with Docker Desktop for Windows

0
Sign in to vote or save

Dan Ford · dlford.io

Introduction

I will explain in this article my setup for Dockerizing a Gatsby project, and a full-stack web app using NextJS, Semantic-UI, Apollo/GraphQL, ExpressJS, MongooseJS/MongoDB, and NGINX.

We will stand up the default Gatsby starter project, and then a quick and dirty no-frills issue tracker app with NextJS.

There is more than one way to peel this potato, this article will cover the configurations and methodologies that have worked well for me. If you have some tricks of your own I’d love to hear them in the comments down below!

I worked in Windows Subsystem for Linux (WSL) exclusively for a long time, but I eventually fully embraced the Windows environment for development due to better integration with VSCodium and some other tools, I haven’t looked back. WSL is great and I still use it when I need a bash shell for command-line tools like SSH, but now even PowerShell has SSH, on the other hand, WSL2 is coming down the pike and looks to be pretty exciting, so who knows what the future holds.

In order to follow along with this tutorial, you will need a Windows 10 machine with the following software installed.

Required Software

Recommended Software

  • Go

  • VSCodium (or Visual Studio Code) with the following extensions

    • Docker (Microsoft)
    • Docker Explorer (Jun Han)
    • Go (Microsoft)
    • NGINX Configuration (William Voyek)
    • nginx-formatter (Simon Schneider)
    • nginx.conf hint (Liu Yue)
    • npm (egamma)
    • npm intellisense (Christian Kohler)
    • Prettier - Code formatter (Esben Petersen)
    • vscode-icons (VSCode Icons Team)

Notes

  1. Keep any environment variables that are required at the time the image is built in Dockerfile and Dockerfile.dev, and any others (especially secrets or sensitive information) in docker-compose.yml and docker-compose.dev.yml.

  2. If there is sensitive information in any Docker configuration file, don’t forget to add it to .gitignore and .dockerignore to keep it private!

  3. Sometimes npm run start:dev will error out on the first run of a new project, I believe this is an issue with filesystem permissions taking too long to finish allowing the new directory while the image is being built, running npm run build:dev to rebuild the image will clear up the issue.

  4. At the time of writing, Docker Desktop for Windows version 2.2.0.0 has a nested mounting bug that breaks these projects

    • There is an open issue on GitHub.
    • In the meantime, you can download Docker Desktop for Windows version 2.1.0.5 from here.

Gatsby

I’ll start with Gatsby since it has less moving parts making for an easier setup. For reference, here is a GitHub repository of the finished project:

Initialize

Hold the shift key and right-click on your desktop, choose “Open PowerShell window here”, then install gatsby-cli globally, create a new project.

Right-click on the “cloud-native-gatsby” folder on your desktop, and choose “Open with VSCodium” (or VSCode), and we’re ready to go.

Create a new folder called docker in the root of the project, we’ll be working mostly in here, also add two more folders inside the docker folder called healthcheck and nginx.

Prettier

I use Prettier to keep my code uniform and clean (or at least the formatting of it anyway). Since we’re Dockerizing, we’ll need to install Prettier globally because the package won’t be available on the machine you’re running VSCodium on.

Press ctrl + , to open VSCodium’s settings and type prettier path into the search box, and change the value to the full path of the global installation, be sure to change <USERNAME> with your username on your workstation:

Now we can add a configuration file in the root of the project, here are the settings I like to use:

Docker Ignore

Copy the default .gitignore file and name it .dockerignore, we want to ignore all of the same files.

Healthcheck

Here’s a super simple healthcheck that takes port from the environment variable HEALTHCHECK_PORT, and makes sure that port responds with code 200 which means everything is OK.

NGINX

In the main NGINX configuration file nginx.conf, we’ll remove the user declaration so we can run it as a non-root user, change worker_processes to 1, and change the access_log to stdout so they’ll show up in Docker logs. I haven’t enabled gzip here, because my production web apps sit behind an NGINX reverse proxy with Brotli enabled, a more efficient compression algorithm, and Brotli won’t re-compress a gzipped file.

In the site configuration file default.conf, we’ll set the port to 8000 so we can run NGINX as a non-root user, tell NGINX how to serve the generated static files from Gatsby, and ensure that the correct Cache-Control headers are set up for each resource to maintain the speed that Gatsby offers out of the box.

Dockerfile

We’ll run two environments, on the development side we’ll be bind mounting the project directory and running as root, and on the production side we’ll build the Go healthcheck and Gatsby site then pass them over to a non-root NGINX container, so we’ll need a separate Dockerfile for each environment.

Development

Not much special about this, except the CHOKIDAR_USEPOLLING environment variable that fixes hot-reload by using polling instead of fsevents or inotify, since neither will work in a Docker bind mount.

Production

Here we are using two build steps, for the Go healthcheck (the extra flags keep the executable smaller) and Gatsby site, pulling in our NGINX configurations, and setting the user and environment variables.

Docker Compose

Development

The trick here is to create volumes for node_modules, public, and .cache before bind mounting the project directory, this way the container runs it’s own version of those directories, to prevent any Windows/Linux conflicts.

Production

Nothing fancy here, all the work was done in the image created by the Dockerfile.

NPM Scripts

First, we need to add -H 0.0.0.0 to the develop script so we can access the site in the development environment.

Now that the configurations are all set to go, let’s make them easy to use with some more NPM scripts.

Development

Production

Publishing

I keep a local production Docker host with a private registry to manage deployments, it uses ouroboros to automatically spin up new images after they land in the registry. Feel free to write your own publish script here!

Standardize Formatting

Before my first git commit, I try to remember to run prettier --write ./**/*, this will format all the code in the project that Prettier can in accordance with your .prettierrc file. Doing this now will ensure that any future commits will be clean and easy to read because there aren’t formatting changes being added to them.

Full-Stack NextJS

Now that we’ve got our feet wet, let’s dive into a full-stack setup. In lieu of adding yet another to-do app to the internets, let’s make a really basic issue tracker, I’ll omit authentication here since it’s already a pretty long article, but I do plan to cover authentication in a dedicated article in the future.

For your reference, here is a GitHub repository of the finished project:

GitHub - dlford/example-cloud-native-fullstack-nextjs: An example of a quick and dirty issue tracker built in a cloud-native environment using Docker, NextJS, Semantic-UI, Apollo/GraphQL, ExpressJS, MongooseJS/MongoDB, and NGINX

An example of a quick and dirty issue tracker built in a cloud-native environment using Docker, NextJS, Semantic-UI, Apollo/GraphQL, ExpressJS, MongooseJS/MongoDB, and NGINX - dlford/example-cloud-...

https://github.com/dlford/example-cloud-native-fullstack-nextjs

An example of a quick and dirty issue tracker built in a cloud-native environment using Docker, NextJS, Semantic-UI, Apollo/GraphQL, ExpressJS, MongooseJS/MongoDB, and NGINX - dlford/example-cloud-...

Initialize

Create a new folder on your desktop, I’ll go with cloud-native-next, and open it up in VSCodium, run git init and npm init -y in the root directory of the project to create a git repository and package.json file.

Create the folders db, nginx, and server in the root directory of the project, and then run npx create-next-app client to initialize a new NextJS app called client.

You should now have four folders db, nginx, server, and client, as well as a package.json file in the root of the project.

I’ll omit my Prettier setup here, see above from Gatsby if you skipped it since the setup is the same.

Server

First, we need a .gitignore file in the server directory, we should only need node_modules in this one.

We’ll also want a .dockerignore file here with node_modules in it.

Let’s get our GraphQL API up and running, we’ll start by installing the necessary packages.

Initialize

We need a basic Babel config, create the file .babelrc in the server directory with the following contents:

Models

We’ll create the folder server/src/models/, and a new file issue.js in it. To make it interesting let’s have an activity feed for each issue. So the issue schema will need a title and a description, we’ll use a virtual field to pull in feed entries (from another model we’ll create next), sort them by newest to oldest, and make sure they get deleted when this issue is deleted. We’ll also create an “Issue created” feed entry automatically.

You should make a point to skim the Mongoose documentation, it’s a really powerful tool with a ton of useful features!

Now for the Feed model, we’ll keep it simple with a reference to the issue it’s related to, a timestamp, and a message.

Now we need to set up the database configuration, we’ll also export our models from this file for ease of use later. The extra options in the Mongoose connection just disable some deprecated features and silence the warnings in the console.

Schema

Next, we’ll need a schema folder for our GraphQL schemas. We’re using extend type for query, and mutation, because we’ll tie all of them together to export from an index file as with the models, and you can only have one of each type declared. We’ll also add a Healthcheck schema for Docker.

I’ll skip adding a Date scalar to save time, it’s fairly easy to do, but this is already a long article!

Resolvers

Keeping with our pattern, we’ll now make a server/src/resolvers directory to work out of for the GraphQL resolvers. We’ll deliver the models object via context to the resolvers in the apollo-express configuration later, so, for now, we’ll just assume they’re there.

The parent, args, context is not needed here, I included it to illustrate what properties are made available to the resolvers.

for the Issue queries we have issues, which should just return a list of issues, but the issue query should also return that issue’s feed, so we need to populate it with mongoose.

Express

Now we just tie all that together, connect to the database, and start our Apollo GraphQL server.

Healthcheck

I’ll use NodeJS for this healthcheck since it’ll already be installed on the container, no need to add any more complexity to it, we’ll just hit our GraphQL server with the healthcheck query and make sure it returns true. While it takes some extra effort I try to avoid using any installed packages for healthchecks when possible, it keeps it cleaner and more portable since this healthcheck only relies on the built-in library for NodeJS I can copy to any project without worrying about dependencies.

Dockerfile

I’m correcting the time zones for all containers in this project, just because it’s a common issue I tend to overlook, make sure you adjust the location as appropriate to your location.

Development
Production

NPM Scripts

Add some NPM scripts in the server/package.json file. The fuser -k... bit is a hack to get nodemon’s live reload to work correctly in Docker for Windows, without this it will fail saying the port is in use.

Development
Production

Database

Not much exciting here, just setting the timezone and using MongoDB’s own “ping” command for a healthcheck.

Dockerfile

NGINX

We’ll use NGINX to reverse proxy requests for any URI starting with /graphql to the back-end, and any other requests to the front-end.

Healthcheck

I believe Go is the lightest way to implement a healthcheck for this container, so we’ll just use the same one from the Gatsby example.

NGINX Server Configuration

Again we’re removing the user configuration from NGINX so we can run it as a non-root user.

NGINX Site Configuration

These are the reverse proxy configurations, note that both support web-socket connections in case we wanted to implement GraphQL subscriptions, hot-reloading in develop mode also depends on web-sockets.

Dockerfile

Hopefully this is starting to look familiar to you?

Client

We’ll just get the front-end Dockerized, for now, once it’s up and running we can build our issue tracker app’s front-end.

Healthcheck

Dockerfile

Don’t forget about the .dockerignore file! We need node_modules and .next in this one.

Development
Production

NextJS Configuration

Here is the fix for hot-reloading, which is applied only if NODE_ENV is not set to production via a ternary operator.

Docker Compose

Development

Production

Main NPM Scripts

These Docker scripts go in the package.json file at the root of the project.

Development

Production

I also like to add individual production build scripts for each container, so you don’t have to build the whole project if you only changed something on the front-end for example.

Bring up environments

Okay, now we run npm run start:dev to spin up the containers. It’s helpful to open a couple of terminals to watch the logs for the client and server, you can do that with docker logs cloud-native-next_server_dev_1 -f, and docker logs cloud-native-next_client_dev_1 -f.

You should now have access to GraphQL Explorer on the back-end at http://localhost:3000/graphql, and the NextJS front-end at http://localhost:3000/.

Front-End Application

Now we can start creating, I’ll be using Semantic-UI to quickly create a user interface that looks nice and clean, they have some great documentation you may reference if you wish.

Install Dependencies

We need to install some dependencies on the client-side, which we can do from the running dev container by using the command docker exec -it cloud-native-next_client_dev_1 /bin/sh to open up a terminal session within it.

When it’s finished, you can get out of the container session using exit.

Library

To keep things organized, let’s put together a library first in a new directory client/lib/.

Apollo Client

This file gets a little hairy, there are a lot of moving parts to get Apollo working with both server-side rendering (SSR) and client-side rendering (CSR), but don’t worry if you don’t understand it completely, I copied this from the Zeit example and had to study it quite a bit to understand what it’s doing.

This is mostly copied from the Zeit example on GitHub. I only changed the uri of HttpLink to handle both SSR and client side paths.

GraphQL
Utilities

I could use MomentJS (npm install moment) for any date manipulations I need to perform, but since I only need it for one format I’ll save some bundle size by writing my own function. The getIdAttrib function attempts to find and return the value of the data-id attribute of any HTML element passed into it, it’s a convenient way to pass data around in React, you’ll see what I mean later on.

Index

Keeping with our pattern, we’ll make in index file for our lib directory to export everything.

User Interface

We’ll need a form to add issues, the update function in the useMutation hook is for updating the Apollo cache with the new data that is submitted (and the elements on the page as a result), it receives the current cache and the response of the mutation as arguments, which we’re pulling createIssue from directly, we then read the query and write it back into cache with the new data appended.

We’ll use the Confirm component from Semantic-UI for removing issues, it’s essentially a dialog box that darkens everything behind it. We’ll trigger it when the removeIssueId state is not null. After confirmation we also want to hide the issue detail component if it’s displaying the issue that was removed, that component is triggered by the issueDetailId being non-null as you may have guessed.

We need to display the list of issues, you’ll see the use of the getIdAttrib function here, note the data-id={id} properties on the icons that bring up the issue detail view and the remove issue dialog box, we’re destructuring target from the event property passed to the handleShowDetail and handleRemoveIssue functions from each element.

Here we’ll show the feed for a selected issue.

Another quick form to add feed entries.

Now we just need to tie it all together, after saving this file you should see the page update in your browser, everything should be working as expected now!

Challenge

I want you to keep on learning, why not build something cool with that Gatsby site? Or maybe add some more features to the issue tracker app, it would probably benefit from having tags and/or status variables for each issue, maybe setting them to done/archived instead of deleting them? These choices are all yours to make, so build whatever satisfies your itch!

Conclusion

There you have it, you’ve now built two cloud-native web apps, nice job! The whole point of this process is that you can now clone the Git repository to any machine running Docker and start up your application, no need to worry about installing the right dependencies or configurations, and when you take it down you aren’t leaving anything behind on the base system, everything is nice and clean.

Disclosure:

Please note that some of the links on this site may be affiliate links, and at no additional cost to you, I will earn a commission if you decide to make a purchase after clicking through the link. Please understand that I only recommend products because I've found them to be helpful and useful, not because of the small commissions I make if you decide to buy something through my links. Please do not spend any money on these products unless you feel you need them or that they will help you achieve your goals.

Read the original on dlford.io

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.