paint-brush
How to Automate A Blog Post App Deployment With GitHub Actions, Node.js, CouchDB, and Aptibleby@wise4rmgod
3,074 reads
3,074 reads

How to Automate A Blog Post App Deployment With GitHub Actions, Node.js, CouchDB, and Aptible

by Wisdom NwokochaDecember 4th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Welcome to the guide on automating blog post app deployment using GitHub Actions, Node.js, CouchDB, and Aptible. This comprehensive tutorial will guide you in building, deploying, and managing a blog post application using the tools and technologies above. But first, let me give you a brief overview of the Blogpost app, what it does, and its main components. The Blogpost app is a web application that allows users to create and share blog posts. Users can write, edit, delete, and view posts by other users. The app uses Node.js as the backend, CouchDB as the database, and GitHub Actions as the continuous integration and deployment tool.
featured image - How to Automate A Blog Post App Deployment With GitHub Actions, Node.js, CouchDB, and Aptible
Wisdom Nwokocha HackerNoon profile picture

Welcome to the guide on automating a blog post app deployment using GitHub Actions, Node.js,

CouchDB, and Aptible.


This comprehensive tutorial will guide you in building, deploying, and managing a blog post application using the tools and technologies above.


But first, let me give you a brief overview of the Blogpost app, what it does, and its main components. The Blogpost app is a web application that allows users to create and share blog posts.


Users can write, edit, delete, and view posts by other users. The app uses Node.js as the backend, CouchDB as the database, and GitHub Actions as the continuous integration and deployment tool.


Why did I choose these? Well, there are many reasons, but here are some of the main ones:


  • Node.js is a fast, scalable, and easy-to-use JavaScript runtime environment that can run on various platforms. It has many libraries and frameworks for web development, such as Express, a minimalist and flexible web application framework I will use in this project.


  • CouchDB is an open-source NoSQL database system that’s reliable, secure, and powerful. It is a document-oriented database that uses JSON to store data.


  • GitHub Actions is a flexible, convenient, and integrated tool that allows you to automate workflows for your GitHub repositories. It also has many pre-built actions that you can use or customize for your needs, such as the Aptible Deploy action, which I will use in this project to deploy the app to Aptible.


  • Aptible is a cloud-based platform for deploying and managing containerized applications, providing easy provisioning, scaling, and monitoring capabilities.

Prerequisites

Before starting on the development journey, setting up the necessary tools and technologies is essential.


  • Node.js: Ensure Node.js is installed on your machine.


  • CouchDB: Verify that CouchDB is installed and running on your system.


  • Node.js and JavaScript Expertise: Possess a solid understanding of Node.js and JavaScript fundamentals.


  • Docker: Install and run Docker on your machine.


  • Aptible Account: Create an Aptible account and familiarize yourself with deploying a basic application.


  • Docker Cloud Account: Acquire a Docker Cloud account to host your applications.

Developing the Blogpost App

Step 1: Set Up Your Node.js Project

  • Create a project directory for your blog post app.


  • Initialize a Node.js project using npm:
npm init -y


  • Install the Express.js framework, which will serve as the foundation for the backend:
npm install express nano

Step 2: Set Up CouchDB

  • Make sure CouchDB is installed and running. You can access CouchDB through your browser at http://127.0.0.1:5984/_utils/.


CouchDB dashboard

Step 3: Create Your Node.js Application

  • Create a blog.js file in your project directory.


  • Initialize Express and connect it to CouchDB:
const express = require("express");
const nano = require("nano")("http://admin:[email protected]:5984");
const app = express();
const port = 3000;

// Middleware to parse JSON data
app.use(express.json());

// async function asyncCall() {
//   // await nano.db.destroy("alice");
//   await nano.db.create("blogposts");
//   const db = nano.use("blogposts");
//   return response;
// }
// asyncCall();

const db = nano.use("blogposts");


  • This code defines API endpoints for creating, retrieving, updating, and deleting blog posts using a CouchDB database.


// Create a new blog post
app.post("/posts", async (req, res) => {
  const { title, description, author } = req.body;

  try {
    const doc = await db.insert({
      title,
      description,
      author,
      createdAt: new Date(),
      updatedAt: new Date(),
    });

    res.json({ id: doc.id, rev: doc.rev });
  } catch (err) {
    console.error(err);
    res.status(500).send("Error creating post");
  }
});

// Get all blog posts
app.get("/posts", async (req, res) => {
  try {
    const docs = await db.list({ include_docs: true });
    res.json(docs.rows);
  } catch (err) {
    console.error(err);
    res.status(500).send("Error retrieving posts");
  }
});

// Get a specific blog post
app.get("/posts/:id", async (req, res) => {
  const { id } = req.params;

  try {
    const doc = await db.get(id);
    res.json(doc);
  } catch (err) {
    console.error(err);
    res.status(404).send("Post not found");
  }
});

// Update a blog post
app.put("/posts/:id", async (req, res) => {
  const { id } = req.params;
  const { title, description } = req.body;

  try {
    await db.insert({
      _id: id,
      title,
      description,
      updatedAt: new Date(),
    });

    res.json({ message: "Post updated successfully" });
  } catch (err) {
    console.error(err);
    res.status(500).send("Error updating post");
  }
});

// Delete a blog post
app.delete("/posts/:id", async (req, res) => {
  const { id } = req.params;

  try {
    await db.destroy(id);
    res.json({ message: "Post deleted successfully" });
  } catch (err) {
    console.error(err);
    res.status(500).send("Error deleting post");
  }
});

app.listen(port, () => {
  console.log(`Blogpost app listening on port ${port}`);
});


Testing it Locally:

Thorough testing is crucial to ensure the functionality and robustness of your project. Here's a sample API documentation to guide your testing process:

API Documentation

Base URL:

Assuming your server is running locally at port 3000, the base URL for your API would be:

http://localhost:3000

API Endpoints:

a. Create a New Blog Post

  • Endpoint: POST /posts

  • Description: Creates a new blog post.

  • Request Body:

    {
      "title": "String",
      "description": "String",
      "author": "String"
    }
    
  • Example:

    POST http://localhost:3000/posts
    {
      "title": "Sample Title",
      "description": "Sample Description",
      "author": "John Doe"
    }
    



b. Get All Blog Posts

  • Endpoint: GET /posts

  • Description: Retrieves all blog posts.

  • Response:

    [
      {
        "id": "String",
        "key": "String",
        "value": {
          "rev": "String"
        },
        "doc": {
          "_id": "String",
          "_rev": "String",
          "title": "String",
          "description": "String",
          "author": "String",
          "createdAt": "Date",
          "updatedAt": "Date"
        }
      }
    ]
    
  • Example:

    GET http://localhost:3000/posts
    



c. Get a Specific Blog Post

  • Endpoint: GET /posts/:id

  • Description: Retrieves a specific blog post by its ID.

  • Parameters:

    • id: ID of the blog post.
  • Response:

    {
      "_id": "String",
      "_rev": "String",
      "title": "String",
      "description": "String",
      "author": "String",
      "createdAt": "Date",
      "updatedAt": "Date"
    }
    
  • Example:

    GET http://localhost:3000/posts/your-post-id
    

d. Update a Blog Post

  • Endpoint: PUT /posts/:id

  • Description: Updates a specific blog post by its ID.

  • Parameters:

    • id: ID of the blog post.
  • Request Body:

    {
      "title": "String",
      "description": "String"
    }
    
  • Example:

    PUT http://localhost:3000/posts/your-post-id
    {
      "title": "Updated Title",
      "description": "Updated Description"
    }
    

e. Delete a Blog Post

  • Endpoint: DELETE /posts/:id

  • Description: Deletes a specific blog post by its ID.

  • Parameters:

    • id: ID of the blog post.
  • Example:

    DELETE http://localhost:3000/posts/your-post-id
    


Please replace your-post-id with an actual ID of the blog post when testing the GET, PUT, and DELETE requests.

Step 4: Dockerize Your Node.js Application

You need to have a Docker Hub account. If you haven't created one yet, sign up at Docker Hub.

Ensure you have Docker installed and running on your local machine.

Steps to Push Dockerized App to Docker Hub:

  • Create a Dockerfile in the root directory of your Node.js project.
# Use an official Node.js runtime as the base image
FROM node:16

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy package.json and package-lock.json to the working directory
COPY package*.json ./

# Install app dependencies
RUN npm install

# Copy the rest of the application files to the working directory
COPY . .

# Expose the port the app runs on
EXPOSE 3000

# Define the command to run the app
CMD ["node", "blog.js"]
  • Tag your Docker image: Open your terminal/command prompt and navigate to the root directory of your Node.js application, where your Dockerfile is located.


Run the following command to build your Docker image and tag it with your Docker Hub username and desired repository name:


 docker build -t your-docker-username/blogpost-app:latest .

Replace your-docker-username with your Docker Hub username and blogpost-app with your desired repository name.


You will get a similar response like this:

[+] Building 1.1s (10/10) FINISHED                                                                                              docker:desktop-linux
 => [internal] load .dockerignore                                                                                                               0.0s
 => => transferring context: 2B                                                                                                                 0.0s
 => [internal] load build definition from Dockerfile                                                                                            0.0s
 => => transferring dockerfile: 489B                                                                                                            0.0s
 => [internal] load metadata for docker.io/library/node:16                                                                                      1.0s
 => [1/5] FROM docker.io/library/node:16@sha256:f77a1aef2da8d83e45ec990f45df50f1a286c5fe8bbfb8c6e4246c6389705c0b                                0.0s
 => [internal] load build context                                                                                                               0.0s
 => => transferring context: 45.31kB                                                                                                            0.0s
 => CACHED [2/5] WORKDIR /usr/src/app                                                                                                           0.0s
 => CACHED [3/5] COPY package*.json ./                                                                                                          0.0s
 => CACHED [4/5] RUN npm install                                                                                                                0.0s
 => CACHED [5/5] COPY . .                                                                                                                       0.0s
 => exporting to image                                                                                                                          0.0s
 => => exporting layers                                                                                                                         0.0s
 => => writing image sha256:c5f046a9b99389aea6bf3f503e9b05cce953daf1b3f77ee5fb3f7469dc36c709                                                    0.0s
 => => naming to docker.io/wise4rmgod/blogpost-app:latest    


  • Log in to Docker Hub: Authenticate your Docker client with your Docker Hub account by executing the following command:
 docker login


Enter your Docker Hub username and password when prompted.

Authenticating with existing credentials...
Login Succeeded


  • Push the Docker image to Docker Hub: Once logged in, push your tagged Docker image to your Docker Hub repository using the following command:
 docker push your-docker-username/blogpost-app:latest

This command uploads your local image to Docker Hub under your specified repository.


  • Verify the Push: Go to your Docker Hub account on the web and navigate to your repository to confirm that your Docker image has been successfully pushed.

    Docker cloud dashboard


Step 5: Deploy your project to Aptible

This tutorial assumes you have a basic understanding of setting up an environment, application, endpoint, and database on the Aptible platform. The tutorial utilizes CouchDB as the database and employs Direct Docker for deployment.


  • Log in to Aptible via CLI using the following command:
aptible login


You will be prompted to enter your email and password. If successful, you will receive a response similar to this:

Token written to /Users/wisdomnwokocha/.aptible/tokens.json
This token will expire after 6 days, 23 hrs (use --lifetime to customize)


  • Now, deploy your app using the following command:

Syntax:

aptible deploy --app <app name> --docker-image <docker image in cloud>


Here is an example command:

aptible deploy --app reactgame --docker-image wise4rmgod/blogpost-app


You will receive a response similar to the following:

INFO -- : Starting App deploy operation with ID: 61135861
INFO -- : Deploying without git repository
INFO -- : Writing .aptible.env file...
INFO -- : Fetching app image: wise4rmgod/blogpost-app...
INFO -- : Pulling from wise4rmgod/blogpost-app
INFO -- : 26ee4ff96582: Pulling fs layer
INFO -- : 446eab4103f4: Pulling fs layer
INFO -- : 2e3c22a0f840: Pulling fs layer
INFO -- : a7ab8ad9b408: Pulling fs layer
INFO -- : 3808fdf0c601: Pulling fs layer
INFO -- : ab9e4075c671: Pulling fs layer
INFO -- : 362360c8cef6: Pulling fs layer
INFO -- : 928b5d11ac66: Pulling fs layer
INFO -- : dc87e077ac61: Pulling fs layer
INFO -- : f108e80f4efc: Pulling fs layer
INFO -- : 84ac53840ac8: Pulling fs layer
INFO -- : e81f21b79a1f: Pulling fs layer
INFO -- : 2e3c22a0f840: Downloading: 523 KB / 49.8 MB
INFO -- : 446eab4103f4: Downloading: 173 KB / 16.6 MB
INFO -- : 26ee4ff96582: Downloading: 483 KB / 47 MB
INFO -- : 2e3c22a0f840: Downloading: 25.7 MB / 49.8 MB
INFO -- : a7ab8ad9b408: Downloading: 528 KB / 175 MB
INFO -- : ab9e4075c671: Downloading: 355 KB / 33.4 MB
INFO -- : a7ab8ad9b408: Downloading: 35.3 MB / 175 MB
INFO -- : 26ee4ff96582: Pull complete
INFO -- : 446eab4103f4: Pull complete
INFO -- : 2e3c22a0f840: Pull complete
INFO -- : a7ab8ad9b408: Downloading: 71.2 MB / 175 MB
INFO -- : a7ab8ad9b408: Downloading: 106 MB / 175 MB
INFO -- : a7ab8ad9b408: Downloading: 142 MB / 175 MB

INFO -- : a7ab8ad9b408: Pull complete
INFO -- : 3808fdf0c601: Pull complete
INFO -- : ab9e4075c671: Pull complete
INFO -- : 362360c8cef6: Pull complete
INFO -- : 928b5d11ac66: Pull complete
INFO -- : dc87e077ac61: Pull complete
INFO -- : f108e80f4efc: Pull complete
INFO -- : 84ac53840ac8: Pull complete
INFO -- : e81f21b79a1f: Pull complete
INFO -- : Digest: sha256:de9d04d069ca89ebdb37327365a815c88cd40d90cbc5395cc31c351fff1206dd
INFO -- : Status: Downloaded newer image for wise4rmgod/blogpost-app:latest
INFO -- : No Procfile found in git directory or /.aptible/Procfile found in Docker image: using Docker image CMD
INFO -- : No .aptible.yml found in git directory or /.aptible/.aptible.yml found in Docker image: no before_release commands will run
INFO -- : Pushing image dualstack-v2-registry-i-0a5ec8cff8e775b34.aptible.in:46022/app-63213/72184c41-7dc6-4313-b10e-749125f72577:latest to private Docker registry...
INFO -- : The push refers to repository [dualstack-v2-registry-i-0a5ec8cff8e775b34.aptible.in:46022/app-63213/72184c41-7dc6-4313-b10e-749125f72577]
INFO -- : dd387bc6b362: Pushed
INFO -- : 586bd9d5efcf: Pushed
INFO -- : 8ae0c889ca84: Pushed
INFO -- : c91ec53bcc27: Pushing: 522 KB / 93.6 MB
INFO -- : aec897bac4f0: Pushed
INFO -- : 0ead224631d3: Pushed
INFO -- : ad3b30eb29d3: Pushing: 542 KB / 444 MB
INFO -- : 2a7587eb01b6: Pushing: 544 KB / 137 MB
INFO -- : be36d2a441aa: Pushed
INFO -- : 03f6e3800bbe: Pushed
INFO -- : a10e482288d1: Pushing: 338 KB / 30.7 MB
INFO -- : f9cfc9f6b603: Pushing: 513 KB / 103 MB
INFO -- : c91ec53bcc27: Pushing: 31.3 MB / 93.6 MB
INFO -- : c91ec53bcc27: Pushing: 62.7 MB / 93.6 MB
INFO -- : ad3b30eb29d3: Pushing: 44.5 MB / 444 MB
INFO -- : 2a7587eb01b6: Pushing: 34.4 MB / 137 MB
INFO -- : a10e482288d1: Pushed
INFO -- : ad3b30eb29d3: Pushing: 88.9 MB / 444 MB
INFO -- : f9cfc9f6b603: Pushing: 34.6 MB / 103 MB
INFO -- : 2a7587eb01b6: Pushing: 68.9 MB / 137 MB
INFO -- : ad3b30eb29d3: Pushing: 133 MB / 444 MB
INFO -- : f9cfc9f6b603: Pushing: 70.2 MB / 103 MB
INFO -- : c91ec53bcc27: Pushed
INFO -- : 2a7587eb01b6: Pushing: 103 MB / 137 MB
INFO -- : ad3b30eb29d3: Pushing: 178 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 224 MB / 444 MB
INFO -- : 2a7587eb01b6: Pushed
INFO -- : f9cfc9f6b603: Pushed
INFO -- : ad3b30eb29d3: Pushing: 270 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 312 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 355 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushing: 401 MB / 444 MB
INFO -- : ad3b30eb29d3: Pushed
INFO -- : latest: digest: sha256:de9d04d069ca89ebdb37327365a815c88cd40d90cbc5395cc31c351fff1206dd size: 2841
INFO -- : Pulling from app-63213/72184c41-7dc6-4313-b10e-749125f72577
INFO -- : Digest: sha256:de9d04d069ca89ebdb37327365a815c88cd40d90cbc5395cc31c351fff1206dd
INFO -- : Status: Image is up to date for dualstack-v2-registry-i-0a5ec8cff8e775b34.aptible.in:46022/app-63213/72184c41-7dc6-4313-b10e-749125f72577:latest
INFO -- : Image app-63213/72184c41-7dc6-4313-b10e-749125f72577 successfully pushed to registry.
INFO -- : STARTING: Register service cmd in API
INFO -- : COMPLETED (after 0.28s): Register service cmd in API
INFO -- : STARTING: Derive placement policy for service cmd
INFO -- : COMPLETED (after 0.15s): Derive placement policy for service cmd
INFO -- : STARTING: Create new release for service cmd
INFO -- : COMPLETED (after 0.24s): Create new release for service cmd
INFO -- : STARTING: Schedule service cmd
..
INFO -- : COMPLETED (after 13.49s): Schedule service cmd
INFO -- : STARTING: Stop old app containers for service cmd
INFO -- : COMPLETED (after 0.0s): Stop old app containers for service cmd
INFO -- : STARTING: Start app containers for service cmd
INFO -- : WAITING FOR: Start app containers for service cmd

INFO -- : WAITING FOR: Start app containers for service cmd
INFO -- : COMPLETED (after 18.4s): Start app containers for service cmd
INFO -- : STARTING: Delete old containers for service cmd in API
INFO -- : COMPLETED (after 0.0s): Delete old containers for service cmd in API
INFO -- : STARTING: Commit app containers in API for service cmd
INFO -- : COMPLETED (after 0.26s): Commit app containers in API for service cmd
INFO -- : STARTING: Commit service cmd in API
INFO -- : COMPLETED (after 0.13s): Commit service cmd in API
INFO -- : STARTING: Cache maintenance page
INFO -- : COMPLETED (after 0.28s): Cache maintenance page
INFO -- : STARTING: Commit app in API
INFO -- : COMPLETED (after 0.19s): Commit app in API
INFO -- : App deploy successful.


  • Visit the Aptible Dashboard to confirm that the deployment was successful.

  • Click on the Endpoint tab in the dashboard and save the endpoint. This will allow you to expose your database to the public internet.

  • Click on Add Endpoint in the next screen to create a new endpoint.

Step 6: Create GitHub Actions Workflow:

  • Push your code to GitHub and navigate to your repository on GitHub.com.
  • Go to the Actions tab in your repository.
  • Click on the "Set up a workflow yourself" or "New workflow" button, then choose "Set up a workflow yourself".
  • GitHub will open an editor to create the workflow YAML file (e.g., .github/workflows/docker-build-push.yml).
  • Replace the content with the following example YAML code:
on:
  push:
      branches: [ main ]

env:
  IMAGE_NAME: user/app:latest
  APTIBLE_ENVIRONMENT: "my_environment"
  APTIBLE_APP: "my_app"


jobs:
  deploy:
    runs-on: ubuntu-latest

      # Allow multi platform builds.
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2

      # Allow use of secrets and other advanced docker features.
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      # Log into Docker Hub
      - name: Login to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      # Build image using default dockerfile.
      - name: Build and push
        uses: docker/build-push-action@v3
        with:
          push: true
          tags: ${{ env.IMAGE_NAME }}

      - name: Deploy to Aptible
        uses: aptible/aptible-deploy-action@v1
        with:
          username: ${{ secrets.APTIBLE_USERNAME }}
          password: ${{ secrets.APTIBLE_PASSWORD }}
          environment: ${{ env.APTIBLE_ENVIRONMENT }}
          app: ${{ env.APTIBLE_APP }}
          docker_img: ${{ env.IMAGE_NAME }}
          private_registry_username: ${{ secrets.DOCKERHUB_USERNAME }}
          private_registry_password: ${{ secrets.DOCKERHUB_TOKEN }}


Adjust the branch name main, Docker Hub username yourusername, and repository name yourrepository , aptible_username, aptible_password, and environment to match your configurations.


Set Up Secrets:

  • Click on the repository Settings.
  • Go to SecretsNew repository secret.
  • Add four secrets:
    • DOCKERHUB_USERNAME: Your Docker Hub username.

    • DOCKERHUB_TOKEN: Your Docker Hub access token. Generate it in Docker Hub under Account Settings ➔ Security ➔ New Access Token. Give it the necessary permissions for pushing images.

    • APTIBLE_USERNAME: Your Aptible username.

    • APTIBLE_PASSWORD: Your Aptible password.


Conclusion

This comprehensive tutorial will help you build, deploy, and manage a blog post application with Node.js, CouchDB, and Aptible.


You've grasped the fundamentals of setting up the essential tools and technologies, crafting the backend of the blog post application, dockerizing the application, pushing the Docker image to Docker Hub, and deploying the application to Aptible.


Congratulations on completing this comprehensive tutorial and your journey into cloud-native application development using Aptible!