Overcomplicating things for no reason (Part 2)

  • by

It’s been a busy week, but also a fruitful one. We have finally deployed a somewhat production ready application to mujik.aloo.pw. Currently you can sign up, login and… that’s about it. We’ll get where we need to in about five weeks! The majority of the time last week was creating a developer workflow that made it easy to have all the infrastructure up and ready for local development without having my team members worrying about installing dependencies or setting up databases. A simple docker-compose.yml did the trick for us.

version: "3"
services:
  ## API ##
  api:
    container_name: mujik-api
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    environment:
      - MONGO_HOSTNAME=db
      - MONGO_PORT=27017
    ports:
      - 3001:3001
    volumes:
      - /app/node_modules
      - ./api:/app
    networks:
      - app-network

  ## WEB ##
  web:
    container_name: mujik-web
    stdin_open: true
    build:
      context: ./web
      dockerfile: Dockerfile.dev
    volumes:
      - /app/node_modules
      - ./web:/app
    ports:
      - 3000:3000
    networks:
      - app-network

  ## MONGODB ##
  db:
    container_name: db
    image: mongo
    ports:
      - 27017:27017

    volumes:
      - ./dbdata:/data/db
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

As you can see, there is only one dependency for our project. That being mongodb. Things are going to get a lot more complicated and spicy when we introduce redis and the ELK stack. We can start development with a simple docker-compose up with hot-reload for both mujik-api and mujik-web. Our production docker-compose is quite similar with the building stage replaced with pre-built images.

CI/CD on the cheap

Our project is already using Github Actions but we wanted to stay as lean as possible when it came to cost. We have a single VPS instance on Hetzner ($5.34/month) and using the following Github Action allowed us to deploy our docker images to the VPS. The VPS is running nginx pointing to each of the services for our app. So if you visit mujik.aloo.pw, nginx creates a reverse proxy to localhost:3000 and api.mujik.aloo.pw is pointing to localhost:3001.

So when a push is made to the master branch, the docker-compose service is restarted (depending on whether it is mujik-web or mujik-api). Here is the Github Action that allows us to build and deploy our service to a VPS.

name: Build and Push to DockerHub

on:
  push:
    branches: master

jobs:
  # Run basic tests.
  test_and_deploy:
    runs-on: ubuntu-latest
    steps:
      # Checkout code and run tests.
      - uses: actions/checkout@v2
      - name: Setup Node.js environment
        uses: actions/setup-node@v1.4.3
        with:
          node-version: 12.18.3
      - name: Install Packages
        run: yarn install
      - name: Run Tests
        run: yarn test

      # Setup environment for docker.
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      # Push image to DockerHub.
      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        env:
          API_URL: ${{ secrets.MUJIK_API_URL }}
        with:
          push: true
          tags: ${{ secrets.MUJIK_WEB_TAG }}
          build-args: |
            API_URL=${{ secrets.MUJIK_API_URL }}

      - name: Image digest
        run: echo ${{ steps.docker_build.outputs.digest }}

      - name: Redeploy
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_KEY }}
          passphrase: ${{ secrets.VPS_KEY_PASS }}
          port: ${{ secrets.VPS_SSH_PORT }}
          script: ${{ secrets.VPS_SCRIPT }}

Our service is extremely small right now and deployments take about 1.5 minutes for our API, and 3 minutes for the web app. This is the case since we have a simple three tier architecture. This will be all we need to finish our project and can scale horizontally very nicely. The only thing we might need to change is our Database but for now this should be enough for a school project.

Making the most of our resources

What’s great is that this single instance also acts as a gateway for my homelab. I am pretty glad I invested in creating a rather modest homelab because now I can host our project on my homelab if the need arises for a beefier machine. I already have the homelab and VPS on a VPN connection and creating a reverse proxy on nginx for an app hosted on my homelab is now quite trivial.

There are great things coming in the pipeline! And I can’t wait to share it all in the next upcoming weeks.