├── .dockerignore ├── .gitignore ├── docker-compose.yml ├── tsconfig.json ├── gulpfile.js ├── app ├── index.ts └── index.html ├── dist ├── index.js ├── index.js.map └── index.html ├── package.json ├── Dockerfile └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | git 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | app: 5 | build: . 6 | command: npm run build 7 | environment: 8 | NODE_ENV: production 9 | ports: 10 | - '3000:3000' 11 | volumes: 12 | - .:/home/app/appDir 13 | - /home/app/appDir/node_modules 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "ES6" 12 | }, 13 | "include": [ 14 | "**/*.ts" 15 | ], 16 | "exclude": [ 17 | "node_modules", 18 | "**/*.spec.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | 3 | const settings = { 4 | copy : { 5 | files: ['app/**/*.html'] 6 | }, 7 | watch: { 8 | files: ['app/**/*.html'] 9 | }, 10 | base: './' 11 | }; 12 | 13 | gulp.task('copy', function () { 14 | return gulp.src(settings.copy.files) 15 | .pipe(gulp.dest('dist')); 16 | }); 17 | 18 | gulp.task('watch', function() { 19 | return gulp.watch(settings.watch.files, function(obj){ 20 | if( obj.type === 'changed') { 21 | gulp.src(obj.path) 22 | .pipe(gulp.dest('dist')); 23 | } 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /app/index.ts: -------------------------------------------------------------------------------- 1 | const app = require('express')(); 2 | const http = require('http'); 3 | const server = http.createServer(app); 4 | const io = require('socket.io')(server); 5 | 6 | app.get('/', function(req, res){ 7 | res.sendFile(__dirname + '/index.html'); 8 | }); 9 | 10 | io.on('connection', function(socket){ 11 | socket.on('chat message', function(msg){ 12 | io.emit('chat message', msg); 13 | }); 14 | }); 15 | 16 | app.set('port', (process.env.PORT || 3000)); 17 | console.log('The port is:::: ', app.get('port')); 18 | 19 | server.listen(app.get('port'), () => { 20 | console.log('---> listening on port ', app.get('port')); 21 | }); 22 | 23 | io.on('connection', (socket) => { 24 | console.log('Client connected'); 25 | socket.on('disconnect', () => console.log('Client disconnected')); 26 | }); 27 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | const app = require('express')(); 2 | const http = require('http'); 3 | const server = http.createServer(app); 4 | const io = require('socket.io')(server); 5 | app.get('/', function (req, res) { 6 | res.sendFile(__dirname + '/index.html'); 7 | }); 8 | io.on('connection', function (socket) { 9 | socket.on('chat message', function (msg) { 10 | io.emit('chat message', msg); 11 | }); 12 | }); 13 | app.set('port', (process.env.PORT || 3000)); 14 | console.log('The port is:::: ', app.get('port')); 15 | server.listen(app.get('port'), () => { 16 | console.log('---> listening on port ', app.get('port')); 17 | }); 18 | io.on('connection', (socket) => { 19 | console.log('Client connected'); 20 | socket.on('disconnect', () => console.log('Client disconnected')); 21 | }); 22 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-node-typescript", 3 | "version": "1.0.2", 4 | "description": "A starter project to quickly build NodeJS apps using Docker and Typescript, with best practices in mind", 5 | "scripts": { 6 | "build": "gulp copy; gulp watch & tsc-watch -p . --onSuccess \"node dist/index.js\"", 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": ["docker", "node", "typescript", "starter", "docker node typescript", "docker node", "docker typescript"], 10 | "author": "Stephen Gardner (opensourceaugie@gmail.com)", 11 | "license": "ISC", 12 | "homepage": "https://github.com/stephengardner/docker-node-typescript", 13 | "dependencies": { 14 | "express": "^4.10.2", 15 | "gulp": "^3.9.1", 16 | "socket.io": "^1.2.0" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/stephengardner/docker-node-typescript" 21 | }, 22 | "devDependencies": { 23 | "@types/express": "^4.11.0", 24 | "@types/node": "^8.5.8" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../app/index.ts"],"names":[],"mappings":"AAAA,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;AACjC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;AACtC,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAExC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,UAAS,GAAG,EAAE,GAAG;IAC5B,GAAG,CAAC,QAAQ,CAAC,SAAS,GAAG,aAAa,CAAC,CAAC;AAC1C,CAAC,CAAC,CAAC;AAEH,EAAE,CAAC,EAAE,CAAC,YAAY,EAAE,UAAS,MAAM;IACjC,MAAM,CAAC,EAAE,CAAC,cAAc,EAAE,UAAS,GAAG;QACpC,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;AAC5C,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAEjD,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE;IAClC,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAC1D,CAAC,CAAC,CAAC;AAEH,EAAE,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE;IAC7B,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,CAAC;AACpE,CAAC,CAAC,CAAC"} -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Docker Chat Service 5 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Docker Chat Service 5 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Specify the image that this app is going to be built from. This is a docker hub hosted Node image 2 | FROM node:8 3 | 4 | # Specify the username this app is going to run in 5 | ENV USER=app 6 | 7 | # Specify the subdirectory (/home/user/sub_dir_here) that this app will run in 8 | ENV SUBDIR=appDir 9 | 10 | # Create a user named $USER. Run npm install as root before doing other commands 11 | RUN useradd --user-group --create-home --shell /bin/false $USER &&\ 12 | npm install --global tsc-watch npm ntypescript typescript gulp-cli concurrently 13 | 14 | # The default directory created for a user in node is /home/user_name 15 | ENV HOME=/home/$USER 16 | 17 | # Copy package.json and the gulpfile as root into the subdir where our app lies 18 | COPY package.json gulpfile.js tsconfig.json $HOME/$SUBDIR/ 19 | 20 | # set the $USER as the owner of the $HOME directory. Necessary after copying the files from the line above 21 | RUN chown -R $USER:$USER $HOME/* 22 | 23 | # Change user to $USER 24 | USER $USER 25 | 26 | # Change directory to the specified subdirectory 27 | WORKDIR $HOME/$SUBDIR 28 | 29 | # As this user, finally run NPM install 30 | RUN npm install 31 | 32 | ## These lines are not necessary because we're creating a volume from the docker-compose.yml file. 33 | ## If we were to not use a volume there, these would be necessary 34 | 35 | # Change the user to root to finalize some commands 36 | # USER root 37 | 38 | # Copy our working directory from the host machine (your machine) into the Docker container 39 | # Not necessary since gulp is taking care of this for us 40 | # COPY . $HOME/$SUBDIR 41 | 42 | # Copying has copied as the root user, so set the owner once again to our specified username 43 | # RUN chown -R $USER:$USER $HOME/**/* 44 | 45 | # Finally, switch back to the non root user and run the final command 46 | # USER $USER 47 | 48 | # Kick node off from the compiled dist folder, which is compiled from our simple gulpfile 49 | CMD ["node", "dist/index.js"] 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Node Typescript Starter Project 2 |

3 | Docker logo 4 | Nodejs logo 5 | Typescript logo 6 |

7 | 8 | ## Installing 9 | `git clone https://github.com/stephengardner/Docker-Node-Typescript.git` (or npm: `npm i docker-node-typescript`) 10 | 11 | `cd docker-node-typescript` 12 | 13 | `docker-compose up` 14 | 15 | ## Overview 16 | You want to build a service (like an API) or web app fast using [Node](https://nodejs.org) and [Typescript](https://typescriptlang.org). This repo sets up your filestructure and project using [containers](https://www.cio.com/article/2924995/software/what-are-containers-and-why-do-you-need-them.html), or, more specifically [Docker](https://www.docker.com/) containers. This setup, and your app, is then easily transferable to any other computer and you can easily replicate an exact build environment. Sound cool? It really is. 17 | 18 | 19 | ## Why I created this 20 | I couldn'd find a great great seed project to begin Node apps using Typescript and Docker. I run a windows machine and sometimes the settings simply weren't working. Issues with `nodemon -L` and Docker not binding to `localhost` would be too common in StackOverflow. I couldn't find a good seed app that explains how to get these things working together, so decided to build one and explain it in extreme verbosity. 21 | 22 | Everything in this README is intended to help you, the user, understand what is going on and why. Many Docker tutorials ignore some fundamentals or core concepts and can be quite confusing. While this isn't a tutorial per-se, it may be a great resource to help you learn the processes going on as you build your app out of this seed. 23 | 24 | ## The result 25 | This app builds a simple web app chat client using `socket.io`. This is just an example to show you how easy it is to get started. Everything is built from that single `docker-compose up` command. You're intended to change the app to your own liking and build literally anything you'd like. `index.ts` is assumed to be your app's starting point. 26 | 27 | 28 | #### File Structure 29 | ``` 30 | docker-node-typescript 31 | | docker-compose.yml 32 | | Dockerfile 33 | | gulpfile.js 34 | | package.json 35 | | README.md 36 | | tsconfig.json 37 | │ 38 | └───app // This is where you put your app files. These will be automatically compiled 39 | │ │ index.ts 40 | │ | index.html 41 | │ 42 | └───dist // This is where your compiled files gets copied to at build-time 43 | ``` 44 | 45 | ## Features 46 | #### No Dependencies Necessary 47 | This requires no dependencies on the host system except Docker and [Docker Compose](https://docs.docker.com/compose/). 48 | When developing, if you want to avoid Typescript yelling at you, it will (as always) be necessary to download the required type definition files. This is easy. Using `npm i @types/node @types/express --save-dev` you can download the types you need for Node and Express. You may do the same for any other type definitions. You should always use `--save-dev` when downloading types. 49 | 50 | #### Not just Typescript 51 | Some other Docker builds are only capable of compiling Typescript because they just use `tsc` without `gulp`. This is great, in most cases. But this seed goes one step further by using `gulp` to move your `html` files into the `dist` folder as well. I know gulpfiles used to have a tendency to scare the crap out of me, so I kept it simple. You can open the **tiny, simple** gulpfile and change the arguments to copy any files you'd like, you don't have to limit yourself to just `html` and `ts`. 52 | 53 | #### Quicker builds 54 | This project uses [`tsc`](https://www.npmjs.com/package/tsc) to build the Typescript. `tsc` offers incremental builds and is faster than `gulp-typescript` at compiling your code. As a side-note, we're also using [`gulp.watch`](https://github.com/gulpjs/gulp/blob/master/docs/API.md#gulpwatchglobs-opts-fn) for non-ts files and [`tsc-watch`](https://www.npmjs.com/package/tsc-watch) for `ts` files. `tsc-watch` allows us to re-load the app (like nodemon would) once compilation is completed. Since `nodemon` requires a `legacy-watch` variable (`-L`) on many windows machines, I opted to go an arguably better route by using `gulp.watch` and `tsc-watch`. 55 | 56 | #### Easily deployable 57 | If you're familiar with [Heroku](https://heroku.com), and you have the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) installed, you can run two commands to get this app deployed. `heroku create` followed by `heroku container:push web`. And... Wa-la! `heroku open` will open your browser to your brand new app. 58 | 59 | ## The `Dockerfile` Breakdown 60 | ``` 61 | FROM node:8 62 | 63 | ENV USER=app 64 | 65 | ENV SUBDIR=appDir 66 | 67 | RUN useradd --user-group --create-home --shell /bin/false $USER &&\ 68 | npm install --global tsc-watch npm ntypescript typescript gulp-cli 69 | 70 | ENV HOME=/home/$USER 71 | 72 | COPY package.json gulpfile.js $HOME/$SUBDIR/ 73 | 74 | RUN chown -R $USER:$USER $HOME/* 75 | 76 | USER $USER 77 | 78 | WORKDIR $HOME/$SUBDIR 79 | 80 | RUN npm install 81 | 82 | CMD ["node", "dist/index.js"] 83 | ``` 84 | 85 | --- 86 | 87 | #### Command: 88 | ``` 89 | FROM node:8 90 | ``` 91 | ##### What: 92 | Build this image from the [`node:8`](https://hub.docker.com/_/node/) image that is maintained by docker on the [Docker Hub](https://hub.docker.com). 93 | Docker is auto-magically creating an instance of Linux with Node installed (that's what the `node:8` means), and this whole instance will still be on your machine. If you're familiar with Virtual Machines, this might sound familiar to you. What is actually happening is a little more complex, but that's a topic for another time. As discussed, this platform will be based in Linux. Every command that follows is essentially a command that will be run from the terminal of this newly created Linux instance. So every command will either be a Linux command or a special Docker command. 94 | ##### Why: 95 | This gives Docker the context it needs to get us up and running. A simple, stable version of Linux with Node installed is a great place to start building your Node and Typescript app! 96 | 97 | --- 98 | 99 | #### Command: 100 | ``` 101 | ENV USER=app 102 | ``` 103 | ##### What: 104 | Set the environment variable of USER to the string "app". We can later reference this by using `$USER` in our Dockerfile. 105 | ##### Why: 106 | This will become clearer later on. This is just syntactic sugar. It prevents us from needing to write "app" multiple times in our Dockerfile. "app" will be the name of our non-root user in Linux that ends up running future commands from this `Dockerfile`. We could theoretically name this user anything. 107 | 108 | --- 109 | 110 | #### Command: 111 | ``` 112 | ENV SUBDIR=appDir 113 | ``` 114 | ##### What: 115 | Set the environment variable of SUBDIR to the string "appDir". We can later reference this by using `$SUBDIR` in our Dockerfile. 116 | ##### Why: 117 | This will become clearer later on. This is just syntactic sugar. It prevents us from needing to write "appDir" multiple times in our Dockerfile. SUBDIR could theoretically be named anything. It's just the name of a subdirectory in which we'll put our files. 118 | 119 | --- 120 | 121 | ##### Command: 122 | ``` 123 | RUN useradd --user-group --create-home --shell /bin/false app &&\ 124 | ``` 125 | ##### What: 126 | This command is part of a two-line command. Let's explain this first line first. This command runs `useradd`, which is a linux command to create a user. What follows are a few options, which I'll explain. 127 | - The `--user-group` command assigns this user it's own user group. A user group is just a way of grouping multiple users together. User groups are often used to perform various actions -- like permissions changes -- for multiple users at the same time. In this case, we're just grouping one user. Even though we're not grouping multiple users together, it's good to assign a user to a user-group instead of it automatically being assigned the default user-group. Calling `--user-group` will assign this user a usergroup which is identical to its name. That's fine. 128 | 129 | - The `--create-home` command dedicates a home directory by default for this soon-to-be-created user. In node, by default, that home directory lives at the path `/home/user_name_here`. This means that `home/$USER` will be a real directory created after this command finishes. 130 | 131 | - The `--shell /bin/false` command sets the `shell` of this user to an existing command defined by `/bin/false`. That's a real command which actually does nothing but exit with an error code. The `shell` is the terminal you see after you SSH or remote login to an FTP server. We set the user to have essentially an error as their shell to prevent anyone from logging in as this user. We do this because no one will ever need to log-in as this user, so this is a security precaution, and is perfectly fine. 132 | 133 | - The final argument: `$USER` is simply finishing the command and telling the system to name this new user whatever `$USER` equates to. Remember, `$USER` is our environment variable we specified up top. It can be anything we want. 134 | 135 | - Lastly, the `&&\` command allows us to chain two commands together. The `&&` means "do the command before me, and then do the command after me". The `\` is just an escape character, letting us write a command that spans two lines. We need it because we have that line-break in there. This line break is just to keep things easily readable. If we don't use this escape character, we'll need to remove that line-break, or we'll get an error. 136 | 137 | ##### Why: 138 | Creating a new user is a recommended practice when creating production-ready Docker apps in Node. Running Node as the root user poses a security concern, and may pose other issues as well. Therefore this line alleviates that concern. 139 | 140 | --- 141 | 142 | #### Command: 143 | ``` 144 | npm install --global npm ntypescript typescript gulp-cli 145 | ``` 146 | ##### What: 147 | We run `npm install --global` on a number of global packages. This `npm install` is running as the default `root` user because we haven't yet switched users using the `USER` command (which we will do shortly). We are using the `--global` tag to identify that the following packages will be installed to the root user's global dependencies. If we tried to run `npm install -g some_package_name` as a non-root user we would run into issues. The non-root user does not have permissions to install global packages. 148 | 149 | ##### Why: 150 | We need to install these global dependencies here because they are crucial for building or running our app inside the container. For example, in order to build our app, the container needs to be able to run `gulp` commands natively, which requires that `gulp-cli` be a global package on the container itself. To help you better understand this, you can try to run something like `gulp` from the command line of your machine. If you don't have the `gulp-cli` installed globally, you'll get an error. Once `gulp-cli` is installed globally, the `gulp` command should be available inside any terminal window. Therefore, since this Docker container is essentially an entire new machine, we need to install gulp on it, along with some other packages. This line accomplishes that task. 151 | 152 | --- 153 | 154 | #### Command: 155 | ``` 156 | ENV HOME=/home/$USER 157 | ``` 158 | ##### What: 159 | Set the environment variable of HOME to the string "/home/$USER" where `$USER` is substituted by Docker for the `USER` environment variable above. We can later reference this by using `$HOME` in our Dockerfile. Remember that the home directory is created by default from Node as `/home/some_user_name` when we created the previous user. 160 | ##### Why: 161 | This is syntactic sugar. It prevents us from needing to write "/home/$USER" multiple times in our Dockerfile. We eventually switch into this home directory because that's where all our files will be stored on the Container. Using this home directory structure is a best-practice. 162 | 163 | --- 164 | 165 | #### Command: 166 | ``` 167 | COPY package.json tsconfig.json gulpfile.js $HOME/$SUBDIR/ 168 | ``` 169 | ##### What: 170 | Copy the `package.json` file, `tsconfig.json` file, and the `gulpfile.js` file on our host machine into the Docker container at `$HOME/$SUBDIR/` 171 | ##### Why: 172 | Our Docker container needs to have access to these files, and, lo and behold, they come from our machine. The `package.json` is a critically important file because the subsequent `npm install` command (which we'll get to) will read this `package.json` file in order to know what to install on the Docker container. The `gulpfile.js` is also critically important because we will be running `gulp` and utilizing the presets we've defined in that file. 173 | 174 | Note - Because the Container itself will run `npm install` against this `package.json` file, you don't actually need to keep dependencies in your project on your host machine, you only need to keep the `package.json` file up to date. Dependencies will be downloaded **inside the container**. You will, however, need to install type declarations like `@types/node` on your host machine if you're writing in Typescript. This is because Typescript will still throw warnings/errors locally if it doesn't know where to look for type definitions. 175 | 176 | As many other blogs note, this `COPY` command comes before the `npm install` so that we can cache the results of `npm install` and only re-run that command when any of the above files has changed. 177 | 178 | --- 179 | 180 | #### Command: 181 | ``` 182 | RUN chown -R $USER:$USER $HOME/* 183 | ``` 184 | ##### What: 185 | The user/usergroup defined by the statement `$USER(user name):$USER(user group name)` will be given ownership permissions of the directory at `$HOME/` and everything else beneath that directory(`*`). 186 | ##### Why: 187 | When we `COPY`'d files from our previous command, we were doing so under the `root` user permissions. We needed to be the `root` user to do that without permissions errors. However, in doing do, those files we copied are owned by the `root` user. We want our **non-root user** to own these files, so that when we eventually switch over to using that non-root user, we have full permissions of these files. 188 | 189 | --- 190 | 191 | #### Command: 192 | ``` 193 | USER $USER 194 | ``` 195 | ##### What: 196 | Change our working user within the Docker container to be the username defined by `$USER`. Every subsequent command (unless we change the user again) will be run as this user. 197 | 198 | ##### Why: 199 | We want to perform the following actions as our non-root user. This will allow us to do so. 200 | 201 | --- 202 | 203 | #### Command: 204 | ``` 205 | WORKDIR $HOME/$SUBDIR 206 | ``` 207 | ##### What: 208 | Change directory in the docker container to the directory defined by `$HOME/$SUBDIR` 209 | 210 | ##### Why: 211 | We're about to perform actions that need us to be in this directory. 212 | 213 | --- 214 | 215 | #### Command: 216 | ``` 217 | RUN npm install 218 | ``` 219 | ##### What: 220 | Install the packages defined by our `package.json` file. This is being run inside the Docker container by our `$USER` user. 221 | ##### Why: 222 | We need to build our NPM dependencies. We run this command **after** we copy in our `package.json` because of how caching works in Docker. Every line in a Dockerfile is cached until Docker recognizes it has been changed. Meaning that when you re-build a Dockerfile, if some line has not changed, then that line will run from the cache, and take very little time to complete. By putting the `package.json` line above this `npm install` line, we are caching the `npm install` command until the `package.json` line (or file) gets changed. If the `package.json` file is changed, Docker will recognize this on it's next build and it will re-run this `npm install` command anew. 223 | 224 | --- 225 | 226 | #### Command: 227 | ``` 228 | CMD ["node", "dist/index.js"] 229 | ``` 230 | ##### What: 231 | Run `node dist/index.js` from the command-line programmatically. This runs our `dist/index.js` file. `dist/index.js` is the compiled version of the Typescript `/index.ts` file, and this file is the entry point of our app. 232 | ##### Why: 233 | As discussed, this kicks off our app. You might be inclined to use something like `RUN npm run some_package-json_script_here` but doing so has some quirks and it's best to use a `CMD` command. 234 | 235 | --- 236 | 237 | 238 | ## The `docker-compose.yml` Breakdown 239 | ``` 240 | version: '3.1' 241 | 242 | services: 243 | app: 244 | build: . 245 | command: npm run build 246 | environment: 247 | NODE_ENV: development 248 | ports: 249 | - '3000:3000' 250 | volumes: 251 | - .:/home/app/appDir 252 | - /home/app/appDir/node_modules 253 | ``` 254 | 255 | #### Command: 256 | ``` 257 | version: '3.1' 258 | ``` 259 | ##### What: 260 | Use `docker-compose` format version `3.1` for the duration of this file. 261 | ##### Why: 262 | Why not? As of writing this, version `3.1` is the latest version. I don't see any reason why you shouldn't use the latest version. 263 | 264 | --- 265 | 266 | #### Command: 267 | ``` 268 | services 269 | ``` 270 | ##### What: 271 | Tell docker that everything within this block will be `services` of Docker. Services are documented [here](https://docs.docker.com/get-started/part3/). They are separate pieces of your presumably distributed application. They are individual "components" of your app. They are "containers in production". A service block describes the way in which that piece of your app should be built and run. Sound confusing? It's not. Check the [documentation](https://docs.docker.com/get-started/part3/). 272 | ##### Why: 273 | Every Docker Compose file needs a service. We will later find that services are created by the Dockerfile and that the `build` property of each `service` block tells `docker-compose` from where to load that `Dockerfile` 274 | 275 | --- 276 | 277 | #### Command: 278 | ``` 279 | app: 280 | ``` 281 | ##### What: 282 | Name this service `app`. Why "app"? Well, We can name this service anything we want. Feel free to change it. 283 | ##### Why: 284 | Every service needs a name. Sometimes in a service we can use this name to reference the service elsewhere in our `docker-compose.yml` file. 285 | 286 | --- 287 | 288 | #### Command 289 | ``` 290 | build: . 291 | ``` 292 | ##### What: 293 | Build this app from the current directory. The `.` means "this directory". The `docker-compose.yml` file will then look here for a `Dockerfile` when building the `app` service. It finds that Dockerfile, and runs it. 294 | ##### Why: 295 | We need to build these services somehow. This is one way to do so. Not every service needs a `build` command, but we do here because we are running this from a specific Dockerfile. Since `build` looks in the subsequent directory for a `Dockerfile`, and since we already have a great Dockerfile, we're good to go. In a different use-case we could run this service from a pre-built image that we have hosted on a repository. Doing that would look something like this: `build: image: username/repo:tag`. 296 | 297 | --- 298 | 299 | #### Command: 300 | ``` 301 | command: npm run build 302 | ``` 303 | ##### What: 304 | This line says that once the `Dockerfile` is complete, we will run the command `npm run build` from the Docker container's command line. 305 | ##### Why: 306 | The `command` command is often used for kicking things off once a Dockerfile is complete. In this case we're running a predefined command called `build` that is defined in our `package.json` file. This `build` command does a couple necessary steps to build the Typescript into JavaScript. It uses `gulp` and `tsc-watch`, among a couple other things. Nothing crazy. 307 | 308 | --- 309 | 310 | #### Command: 311 | ``` 312 | environment: 313 | ``` 314 | ##### What: 315 | Set some environment variables on the Docker container based on the commands that follow 316 | ##### Why: 317 | We often want environment-specific variables set which help us build our app. Things like API keys that we want to keep out of the source-code of our app. Or things that might change when we are running in development vs production. All of these are great candidates for environment variables. 318 | 319 | --- 320 | 321 | #### Command: 322 | ``` 323 | NODE_ENV: production 324 | ``` 325 | ##### What: 326 | Set the `NODE_ENV` environment variable to equal the string "production". This means that within our Docker container, `process.env.NODE_ENV` will equal `production`. 327 | ##### Why: 328 | Based on several **best-practices** guidelines, we want our standard `docker-compose.yml` file to be a production-based build. Development environments will be set up through the merging of separate compose files. 329 | 330 | --- 331 | 332 | #### Command: 333 | ``` 334 | ports: 335 | ``` 336 | ##### What: 337 | Define what ports will be exposed from this Docker container. The ports will follow on the next line(s). 338 | ##### Why: 339 | In order to view our web-based app, we will want to see it on our local machine's through a browser directed to `localhost`. You may not know it, but when you view a website, you're operating by default through port `80`. If you run a host on your own machine and go to `localhost`, that "url" is equivalent to `localhost:80`, meaning that we're connecting to the localhost server through port 80. If we want to expose any ports on our Docker container, we need to expose those ports, too. We can expose, for example, the port `3000` on the host machine and we can later map this to port `3000` on the host machine. We cover this next. 340 | 341 | --- 342 | 343 | #### Command: 344 | ``` 345 | 3000:3000 346 | ``` 347 | ##### What: 348 | Link the **host** machine's port `3000` to hook into the **container's** port `3000`. The number before the semicolon `:` is the port on the host, and the number after the semicolon is the port on the container. 349 | ##### Why: 350 | The container is running its webserver on port `3000` for development. By exposing this port `3000` to our host port `3000`, we're able to point our machine to the address `localhost:3000` (or `192.168.99.100:3000` if you're on windows with docker toolbox) and we can view the development version of our site. 351 | 352 | --- 353 | 354 | #### Command: 355 | ``` 356 | volumes: 357 | ``` 358 | ##### What: 359 | Bind the following volumes to the Docker container. A volume is documented [here](https://docs.docker.com/engine/admin/volumes/). Normally, when you build a container, the contents within that container are not persistent. This means that if you stop that container and then re-start it, the data within it would cease to exist. Volumes allow data to persist between sessions so that this data is not lost. 360 | ##### Why: 361 | We want parts of our container to persist, such as the `node_modules` folder. We also want to have a directory on our host machine persist inside the Docker containe. We cover this next. 362 | 363 | --- 364 | 365 | #### Command: 366 | ``` 367 | - .:/home/app/appDir 368 | ``` 369 | ##### What: 370 | Mount the directory that this `docker-compose.yml` file is located (`.` means "this directory") to the `/home/app/appDir` directory of the Docker container. The string before the period (`.`) refers to the directory on the host machine and the string after the period references the directory on the Docker container. 371 | ##### Why: 372 | We want everything in this directory to persist on to the Docker container. This line accomplishes that. 373 | 374 | --- 375 | 376 | #### Command: 377 | ``` 378 | - /home/app/appDir/node_modules 379 | ``` 380 | ##### What: 381 | Persist the `/home/app/appDir/node_modules` directory across container instances within this container. If the container gets rebuilt, still save this folder. 382 | ##### Why: 383 | This saves time when caching and rebuilding the container. `node_modules` will be persisted. 384 | 385 | ## Deploying to Heroku 386 | `heroku create` 387 | 388 | `heroku container:push web` 389 | 390 | profit 391 | 392 | 393 | ## FAQ 394 | - Q: What about COPYing the files after `npm install`? Most tutorials do that. 395 | 396 | A: You're correct, most tutorials **do** need this. We don't need to do this because our `docker-compose.yml` creates a volume on this working directory. 397 | If you really still wanted to COPY files like that, you can use the following code in-between `RUN npm install` and `CMD`. 398 | ``` 399 | USER root 400 | COPY . $HOME/$SUBDIR 401 | RUN chown -R $USER:$USER $HOME/**/* 402 | USER $USER 403 | ``` 404 | 405 | - Q: I can't see my local app at `localhost:3000` 406 | 407 | A: If your container is running and `localhost` doesn't load, you may be on Windows using Docker Toolbox. If that's the case you should navigate to `192.168.99.100:3000` to see your app. 408 | 409 | - Q: There's HTML in here. We're serving the front-end HTML files from this container, too? 410 | 411 | A: For this *example*, yes. A true microservices buff would know that we probably want to keep these features into separate services. A typescript service would most likely build something like an API or background processing service, but yet, in this example I provided HTML just to get you started. 412 | 413 | --- 414 | 415 | ## Any Questions? 416 | Let me know, I'll be happy to help you understand. 417 | 418 | ## Want to contribute? Could something be improved? 419 | Let me know, I'm happy to learn and make this better. 420 | 421 | ## Inpired by 422 | - [Lessons Learned While Building a Node App](http://jdlm.info/articles/2016/03/06/lessons-building-node-app-docker.html) 423 | 424 | - [Node Docker Good Defaults](https://github.com/BretFisher/node-docker-good-defaults) 425 | 426 | - [Docker and Node.js Best Practices](https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md) 427 | 428 | - The other countless tutorials I wrapped my brain around while tackling this whole Docker beast. 429 | 430 | ## License 431 | 432 | The ISC license: 433 | 434 | Copyright (c) 2018, Stephen Gardner 435 | 436 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 437 | 438 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 439 | --------------------------------------------------------------------------------