# How to Dockerize your app and deploy to Amazon EC2 with CI-CD using GitHub Actions

### **Introduction**

Have you ever used: "It works on my machine,". Now, with containerization and cloud computing technologies like Docker and Amazon EC2, those days are gone. Docker, a leading containerization platform, provides a convenient way to package applications and their dependencies into containers which can run on any machine .

In this blog we are going to dockerize an application and then deploy it in Amazon EC2 with the help of CI-CD pipeline using GitHub Actions. Also in the bonus section, we will see how we can add domain name to our server and also add a free SSL certificate provided by Let's Encrypt.

## **Sections**

1. Initialize the codebase
    
2. Dockerize the code
    
3. CI Pipeline
    
4. Setup EC2 instance
    
5. CD Pipeline
    
6. Setup Nginx
    
7. Add Domain Name (Optional)
    
8. Add SSL Certificate (Optional)
    

### **Initialize the codebase**

You can start by initializing your codebase using a template or your existing project. You can use my sample Node.js server template available on GitHub.

```bash
git clone https://github.com/chaitak-gorai/server-template.git
```

This template provides a simple Node.js server with an MVC architecture. Before proceeding, make sure to update the `Readme.md` file for comprehensive information.

To run the project locally, execute the following commands:

```bash
npm i
npm run server
```

### **Dockerize the code**

#### What is Docker

Docker is a containerization platform that allows you to package your application and its dependencies into a standardized unit called a docker image. This image can be run in a dedicated environment called a docker container.

Ensure Docker is installed on your machine; if not, follow the instructions for your operating system:

* Ubuntu: [Docker Installation](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)
    
* Windows: [Docker Desktop Installation](https://docs.docker.com/desktop/install/windows-install/)
    

To Dockerize your application, create a `Dockerfile` in the project's root directory. You can either create it manually or use the following command:

```bash
docker init
```

Note: This command generates three essential files: `Dockerfile`, `.dockerignore`, and `compose.yaml`. The `.dockerignore` file specifies images to ignore while building the Docker image. The `compose.yaml` file is used for Docker Compose.

Now, let's write the contents of the `Dockerfile`:

```yaml
# Use the base Node.js image
FROM node:18.18.2-alpine

# Set environment variables
ENV NODE_ENV development
ENV PORT 3000
ENV JWT_SECRET secret

# Create a directory to hold the application code inside the image
WORKDIR /app

# Copy package.json and package-lock.json files into the image
COPY package*.json ./

# Install application dependencies
RUN npm i

# Copy the rest of the source files into the image
COPY . .

# Expose the port that the application listens on
EXPOSE 3000

# Run the application
CMD npm run server
```

Explanation of `Dockerfile` contents:

* `FROM`: Specifies the base runtime image. You can choose the appropriate base image for your project.
    
* `ENV`: Sets environment variables for the application.
    
* `WORKDIR`: Creates a directory to hold the application code inside the image.
    
* `COPY`: Copies necessary files into the image.
    
* `EXPOSE`: Exposes the port that the application listens on.
    
* `CMD`: Defines the command to run the application.
    

Build the Docker image using the following command:

`docker build [tag flag : -t for latest] [image-name] [code-directory]`

```bash
docker build -t server-template .
```

Verify the created image:

```bash
docker image ls
```

Now, run the Docker image in a container:

```bash
docker run server-template
```

You can verify the app running from:

```bash
docker ps
```

You can find that the node server is running. But if we go to our browser `localhost:3000` we couldn't find any response. This is because the server is running in the 3000 port of the docker container not in our machine. For this we need to map the port

`-p host port: container port`

```bash
docker run -p 3000:3000 server-template
```

Now you can verify the app running by going the browser.

Whoaa!!. We have created an Image which can be shared to anybody to run on their containers. But how can we share docker images.

### Docker Hub

As for git there is GitHub which contains all codebase similarly you can push your docker images to Docker Hub.

You need to create a user account at docker hub [https://hub.docker.com/signup](https://hub.docker.com/signup)

You can then login to docker via:

```bash
docker login -u [username] -p [password]
```

Now before pushing to Docker Hub we need to tag (rename) our image as username/imagename

```bash
docker tag image-name username/image-name
```

Now push the image

```bash
docker push username/image-name
```

Note here the default tag `:latest` is used. You can give custom tags by specifying at the end of the image name.

### **CI Pipeline**

Now, let's create a Continuous Integration (CI) pipeline using GitHub Actions. This pipeline will trigger whenever new code is pushed to the main branch, build the Docker image, and push it to Docker Hub.

Create a `ci.yml` file inside the `.github/workflows` directory with the following content:

```bash
name: CI_PIPELINE

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Login to Dockerhub
      env:
        DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}
        DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}}
      run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD

    - name: Build the Docker Image
      run: docker build -t server-template .

    - name: Tag the image
      env:
        DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}
      run: docker tag server-template $DOCKER_USERNAME/server-template

    - name: Push the Image
      env:
        DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}
      run: docker push $DOCKER_USERNAME/server-template
```

Make sure to add Docker Hub credentials as secrets in your GitHub repository settings. For that:

* Go to Repo Settings -&gt; Click on Secrets and Variables -&gt; Choose actions -&gt; Click `create new repository secret`
    
* add your `DOCKER_USERNAME` and `DOCKER_PASSWORD`.
    

### **Setup EC2 instance**

We will utilize Amazon EC2 instances to deploy our Dockerized application. Follow these steps to set up an EC2 instance:

1. Log in to the AWS console or create an account if you haven't already.
    
2. Search for EC2 and click on "Launch Instance"
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1707767024652/90264002-1c53-487e-a580-da55b76a37a6.png align="center")
    
    .
    
3. Choose Ubuntu Linux .
    
4. Select the desired machine (we have used t2-micro)
    
5. Create key pair for ssh login
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1707767255098/e8c7852b-a1dd-4c9b-8362-c766a920a928.png align="center")
    
6. It will get dowloaded in Downloads,
    
7. Check on allow Https and Http traffic
    
8. ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1707767271201/e10d5eb4-841b-424c-b6a9-0170cb0d9694.png align="center")
    
    Finally launch the instance
    
9. Click on the instance and then click connect
    
10. Choose SSH client
    
11. Then run the following command from your downloads directory and replace the filename and instance name
    
    ```bash
    ssh -i "ssh-key-filename" ubuntu@instance
    ```
    
    Note: You might need to give the permission to access the file
    
    ```bash
     chmod 400 "ssh-key-filename"
    ```
    
    1. Run to update all packages
        
    
    ```bash
    sudo apt update
    sudo apt-get upgrade
    ```
    

Once connected to the EC2 instance, update all packages and install Docker following the instructions provided earlier.

After installing Docker, add the EC2 instance as a self-hosted runner in your GitHub repository to facilitate deployment.

* Go to repo settings -&gt; Click Actions -&gt; Choose Runners
    
* Click `new self-hosted runner`
    
* Choose Linux
    
* Run all the commands for `Download` and `Configure`
    
* We need to run the runner in background for that use
    

```bash
 ./run.sh &
```

Verify the runner is added by going to repo settings -&gt; Actions -&gt; Runners

### **CD Pipeline**

Create a Continuous Deployment (CD) pipeline using GitHub Actions. This pipeline will run after the CI pipeline is finished, pull the Docker image from Docker Hub, delete the existing container, and run the new image.

Create a `cd.yml` file inside the `.github/workflows` directory with the following content:

```bash
name: CD_PIPELINE

on:
  workflow_run:
    workflows: ["CI_PIPELINE"]
    types:
      - completed

jobs:
  build:
    runs-on: self-hosted

    steps:
    - name: Pull the Docker image
      env:
        DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}
      run: sudo docker pull $DOCKER_USERNAME/server-template:latest

    - name: Delete the old Docker container
      run: sudo docker rm -f server-template-container || true
      
    - name: Run the Docker container
      env:
        DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}
      run: sudo docker run -d -p 3000:3000 --name server-template-container $DOCKER_USERNAME/server-template
```

Push a dummy commit to the main branch and check the `Actions` tab in `github` for running actions. If everything goes right you can check `[IP]:3000` whether the server is running or not.

### **Nginx**

To make our application available from the root URL (/), we'll use Nginx as a reverse proxy.

Install Nginx:

```bash
sudo apt install nginx
```

Open the default Nginx configuration file:

```bash
sudo nano /etc/nginx/sites-available/default
```

Add the following code within the `server` block:

```bash
location / {
    proxy_pass http://localhost:3000;
}
```

Test the Nginx configuration:

```bash
sudo nginx -t
```

If the test is successful, reload Nginx:

```xml
sudo nginx -s reload
```

Now, your application should be accessible from the root URL.

### **Domain Name (Optional)**

If you have a domain name, you can configure it to point to your EC2 instance:

1. Add an A record with your server's IP address in your DNS provider's control panel.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1707767500883/f0df96bb-fbf4-410d-9523-68613cc62d66.png align="center")
    
    It takes some time to reflect.
    
2. Update the Nginx configuration file to include your domain name.
    
    `server_name domain_name;`
    
3. Reload Nginx.
    
    `sudo nginx -s reload`
    

### **SSL Certificate (Optional)**

To secure your application with SSL/TLS encryption, you can obtain an SSL certificate from Let's Encrypt using Certbot:

```bash
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
```

Replace `yourdomain.com` with your actual domain name. Certbot will automatically configure Nginx to use HTTPS.

Yehhh!! With these steps completed, your application should be fully Dockerized, deployed to Amazon EC2, and accessible with a custom domain over HTTPS.

Hope you enjoyed the process. Feel free to provide any suggestion through the comment section.

Thank you :)
