# A First Look at Docker

> Docker is a set of tools that deliver software in isolated packages called containers that bundle their software, libraries and configuration

- **Collection:** Blog post
- **Published:** 2021-07-12
- **Author:** Anthony Campolo
- **Canonical URL:** https://ajcwebdev.com/first-look-docker/
- **Markdown URL:** https://ajcwebdev.com/first-look-docker/index.md
- **JSON URL:** https://ajcwebdev.com/first-look-docker/index.json

---

## Introduction

[Docker](https://www.docker.com/) is a set of tools that use OS-level virtualization to deliver software in isolated packages called containers. Containers bundle their own software, libraries and configuration files. They communicate with each other through well-defined channels and use fewer resources than virtual machines.

The code for this article is available [on my GitHub](https://github.com/ajcwebdev/a-first-look/tree/main/deployment/docker/) and the container image can be found on the [GitHub Container Registry](https://github.com/users/ajcwebdev/packages/container/package/ajcwebdev-docker) and [Docker Hub](https://hub.docker.com/r/ajcwebdev/ajcwebdev-docker).

### Create Node Project

We will create a simple Node application with Express that returns an HTML fragment.

### Initialize Project and Install Dependencies

```bash wrap=false
mkdir ajcwebdev-docker
cd ajcwebdev-docker
npm init -y
npm i express
touch index.js
```

### Create Server

Enter the following code into `index.js`.

```javascript
// index.js

const express = require("express")
const app = express()

const PORT = 8080
const HOST = '0.0.0.0'

app.get('/', (req, res) => {
  res.send('<h2>ajcwebdev-docker</h2>')
})

app.listen(PORT, HOST)
console.log(`Running on http://${HOST}:${PORT}`)
```

### Run Server

```bash wrap=false
node index.js
```

```
Listening on port 8080
```

![01 - localhost-8080-ajcwebdev-docker](https://ajc.pics/2021/07/12/01-localhost-8080-ajcwebdev-docker.webp)

## Create and Build Container Image

You'll need to build a Docker image of your app to run this app inside a Docker container using the official Docker image. We will need two files: `Dockerfile` and `.dockerignore`.

### Create Dockerfile and dockerignore files

Docker can build images automatically by reading the instructions from a [`Dockerfile`](https://docs.docker.com/reference/dockerfile/). A `Dockerfile` is a text document that contains all the commands a user could call on the command line to assemble an image. Using `docker build` users can create an automated build that executes several command-line instructions in succession.

```bash wrap=false
touch Dockerfile
```

The `FROM` instruction initializes a new build stage and sets the Base Image for subsequent instructions. A valid `Dockerfile` must start with `FROM`. The first thing we need to do is define from what image we want to build from. We will use version `14-alpine` of `node` available from [Docker Hub](https://hub.docker.com/_/node) because the universe is chaos and you have to pick something so you might as well pick something with a smaller memory footprint.

```docker wrap=false
FROM node:14-alpine
```

The `LABEL` instruction is a key-value pair that adds metadata to an image.

```docker wrap=false
LABEL org.opencontainers.image.source https://github.com/ajcwebdev/ajcwebdev-docker
```

The `WORKDIR` instruction sets the working directory for our application to hold the application code inside the image. 

```docker wrap=false
WORKDIR /usr/src/app
```

This image comes with Node.js and NPM already installed so the next thing we need to do is to install our app dependencies using the `npm` binary. The `COPY` instruction copies new files or directories from `<src>`. The `COPY` instruction bundles our app's source code inside the Docker image and adds them to the filesystem of the container at the path `<dest>`.

```docker wrap=false
COPY package*.json ./
```

The `RUN` instruction will execute any commands in a new layer on top of the current image and commit the results. The resulting committed image will be used for the next step in the `Dockerfile`. Rather than copying the entire working directory, we are only copying the `package.json` file. This allows us to take advantage of cached Docker layers.

```docker wrap=false
RUN npm i
COPY . ./
```

The `EXPOSE` instruction informs Docker that the container listens on the specified network ports at runtime. Our app binds to port `8080` so you'll use the `EXPOSE` instruction to have it mapped by the `docker` daemon.

```docker wrap=false
EXPOSE 8080
```

Define the command to run the app using `CMD` which defines our runtime. The main purpose of a `CMD` is to provide defaults for an executing container. Here we will use `node index.js` to start our server.

```docker wrap=false
CMD ["node", "index.js"]
```

Our complete `Dockerfile` should now look like this:

```docker wrap=false
FROM node:14-alpine
LABEL org.opencontainers.image.source https://github.com/ajcwebdev/ajcwebdev-docker
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm i
COPY . ./
EXPOSE 8080
CMD [ "node", "index.js" ]
```

Before the docker CLI sends the context to the docker daemon, it looks for a file named `.dockerignore` in the root directory of the context. Create a `.dockerignore` file in the same directory as our `Dockerfile`.

```bash wrap=false
touch .dockerignore
```

If this file exists, the CLI modifies the context to exclude files and directories that match patterns in it. This helps avoid sending large or sensitive files and directories to the daemon.

```
node_modules
Dockerfile
.dockerignore
.git
.gitignore
npm-debug.log
```

This will prevent our local modules and debug logs from being copied onto our Docker image and possibly overwriting modules installed within our image.

### Build Project

The [`docker build`](https://docs.docker.com/reference/cli/docker/buildx/build/) command builds an image from a Dockerfile and a "context". A build's context is the set of files located in the specified `PATH` or `URL`. The `URL` parameter can refer to three kinds of resources:
* Git repositories
* Pre-packaged tarball contexts
* Plain text files

Go to the directory with your `Dockerfile` and build the Docker image.

```bash wrap=false
docker build . -t ajcwebdev-docker
```

The `-t` flag lets you tag your image so it's easier to find later using the `docker images` command.

### List Images

Your image will now be listed by Docker. The [`docker images`](https://docs.docker.com/reference/cli/docker/image/ls/) command will list all top level images, their repository and tags, and their size.

```bash wrap=false
docker images
```

```
REPOSITORY                   TAG       IMAGE ID       CREATED         SIZE

ajcwebdev-docker   latest    cf27411146f2   4 minutes ago   118MB
```

## Run the Image

Docker runs processes in isolated containers. A container is a process which runs on a host. The host may be local or remote.

### Run Docker Container

When an operator executes [`docker run`](https://docs.docker.com/engine/containers/run/), the container process that runs is isolated in that it has its own file system, its own networking, and its own isolated process tree separate from the host.

```bash wrap=false
docker run -p 49160:8080 -d ajcwebdev-docker
```

`-d` runs the container in detached mode, leaving the container running in the background. The `-p` flag redirects a public port to a private port inside the container.

### List Containers

To test our app, get the port that Docker mapped:

```bash wrap=false
docker ps
```

```
CONTAINER ID   IMAGE                        COMMAND                  CREATED          STATUS          PORTS                                         NAMES

d454a8aacc28   ajcwebdev-docker   "docker-entrypoint.s…"   13 seconds ago   Up 11 seconds   0.0.0.0:49160->8080/tcp, :::49160->8080/tcp   sad_kepler
```

### Print Output of App

```bash wrap=false
docker logs <container id>
```

```
Running on http://0.0.0.0:8080
```

Docker mapped the `8080` port inside of the container to the port `49160` on your machine.

### Call App using curl

```bash wrap=false
curl -i localhost:49160
```

```
HTTP/1.1 200 OK

X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 25
ETag: W/"19-iWXWa+Uq4/gL522tm8qTMfqHQN0"
Date: Fri, 16 Jul 2021 18:48:54 GMT
Connection: keep-alive
Keep-Alive: timeout=5

<h2>ajcwebdev-docker</h2>
```

![02 - localhost-49160](https://ajc.pics/2021/07/12/02-localhost-49160.webp)

## Create Docker Compose File

[Compose](https://docs.docker.com/compose/) is a tool for defining and running multi-container Docker applications. After configuring our application's services with a YAML file, we can create and start all our services with a single command.

```bash wrap=false
touch docker-compose.yml
```

Define the services that make up our app in `docker-compose.yml` so they can be run together in an isolated environment.

```yaml
version: "3.9"
services:
  web:
    build: .
    ports:
      - "49160:8080"
```

### Create and Start Containers

Stop your currently running container before running the next command or the port will be in use.

```bash wrap=false
docker stop <container id>
```

The `docker compose up` command aggregates the output of each container. It builds, (re)creates, starts, and attaches to containers for a service.

```bash wrap=false
docker compose up
```

```
Attaching to web_1
web_1  | Running on http://0.0.0.0:8080
```

## Push Project to a GitHub Repository

We can publish this image to the GitHub Container Registry with GitHub Packages. This will require pushing our project to a GitHub repository. Before initializing Git, create a `.gitignore` file for `node_modules` and our environment variables.

```bash wrap=false
echo 'node_modules\n.DS_Store\n.env' > .gitignore
```

It is a good practice to ignore files containing environment variables to prevent sensitive API keys being committed to a public repo. This is why I have included `.env` even though we don't have a `.env` file in this project right now.

### Initialize Git

```bash wrap=false
git init
git add .
git commit -m "I can barely contain my excitement"
```

### Create a New Repository

You can create a blank repository by visiting [repo.new](https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fnew) or using the [`gh repo create`](https://cli.github.com/manual/gh_repo_create) command with the [GitHub CLI](https://cli.github.com/). Enter the following command to create a new repository, set the remote name from the current directory, and push the project to the newly created repository.

```bash wrap=false
gh repo create ajcwebdev-docker \
  --public \
  --source=. \
  --remote=upstream \
  --push
```

If you created a repository from the GitHub website instead of the CLI then you will need to set the remote and push the project with the following commands.

```bash wrap=false
git remote add origin https://github.com/ajcwebdev/ajcwebdev-docker.git
git push -u origin main
```

## Publish to GitHub Container Registry

[GitHub Packages](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages) is a platform for hosting and managing packages that combines your source code and packages in one place including containers and other dependencies. You can integrate GitHub Packages with GitHub APIs, GitHub Actions, and webhooks to create an end-to-end DevOps workflow that includes your code, CI, and deployment solutions.

GitHub Packages offers different package registries for commonly used package managers, such as npm, RubyGems, Maven, Gradle, and Docker. GitHub's [Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) is optimized for containers and supports Docker and OCI images.

### Login to ghcr

To login, create a [PAT (personal access token)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with the ability to upload packages to GitHub Package Registry. Include your key instead of `xxxx`.

```bash wrap=false
export CR_PAT=xxxx
```

Login with your own username in place of `ajcwebdev`.

```bash wrap=false
echo $CR_PAT | docker login ghcr.io -u ajcwebdev --password-stdin
```

### Tag Image

```bash wrap=false
docker tag ajcwebdev-docker ghcr.io/ajcwebdev/ajcwebdev-docker
```

### Push to Registry

```bash wrap=false
docker push ghcr.io/ajcwebdev/ajcwebdev-docker:latest
```

### Pull Image from Registry

To test that our project has a docker image published to a public registry, pull it from your local development environment.

```bash wrap=false
docker pull ghcr.io/ajcwebdev/ajcwebdev-docker
```

```
Using default tag: latest
latest: Pulling from ajcwebdev/ajcwebdev-docker
Digest: sha256:3b624dcaf8c7346b66af02e9c31defc992a546d82958cb067fb6037e867a51e3
Status: Image is up to date for ghcr.io/ajcwebdev/ajcwebdev-docker:latest
ghcr.io/ajcwebdev/ajcwebdev-docker:latest
```

This article only covers using Docker for local development. However, we could take this exact same project and deploy it to various other container services offered by cloud platforms.

Examples of this would include AWS Fargate or Google Cloud Run. There are also services such as Fly and Flightcontrol that provide higher level abstractions for deploying and hosting your containers.

## Discover Related Content

- [Deploy a Docker Container on AWS Lambda](https://ajcwebdev.com/deploy-docker-with-aws-lambda/)
- [A First Look at AWS Fargate](https://ajcwebdev.com/first-look-aws-fargate/)
- [Deploy a GraphQL Server with Docker and Fly](https://ajcwebdev.com/deploy-gql-docker-container-with-fly/)
- [Teach Jenn Docker Basics](https://ajcwebdev.com/videos/teach-jenn-tech-teach-jenn-docker-basics/) (Video)
- [Flightcontrol with Brandon Bayer and Mina Abadir](https://ajcwebdev.com/podcasts/flightcontrol-brandon-bayer-mina-abadir/) (Podcast)
