Deploying a three-tier application in Docker containers

Ugochi UkaegbuUgochi Ukaegbu
7 min read

Docker containers are lightweight, portable, and self-sufficient software units that package everything needed to run an application, including code, runtime, system tools, libraries, and settings.

These containers use Docker technology to ensure consistency across different environments, allowing applications to run reliably on any infrastructure that supports Docker.

In simpler terms, Docker containers make it easier to build, ship, and run software by keeping everything it needs together in a tidy, portable package.

This project can be helpful to intermediate learners who require solid projects to add to their portfolios.

Introduction

An end-to-end application is a complete software program that handles everything from start to finish for a specific task or service.

Take Instagram for instance, from taking a photo, uploading it, adding filters, and writing a caption, to sharing it with friends; that's the end-to-end experience of using Instagram.

Similarly, an end-to-end application in software development includes every phase and component needed for a particular task or service.

For example, if you were building a social media platform from scratch, your end-to-end application would include everything: the front-end where users see and interact with posts, the back-end that stores and manages those posts, databases to keep everything organized, and servers to make it all accessible online.

Deploying such an application in Docker containers means packaging each part (like front-end, back-end, databases) into separate, neat packages that can run independently but still work together seamlessly.

This makes it easier to develop, test, and run the whole application consistently across different computers and servers.

Pre-requisite

  • An EC2 instance

  • Source code.

  • Install Docker

  • Install Docker Compose

Stages to deploy three-tier app on Docker Containers

This guide outlines the key stages of deploying a three-tier application using Docker containers.

Step 1: Create a Virtual Machine

Create a virtual machine on any of the cloud providers. For this project, I used an ec2 of instance type t2.medium. Increase the capacity of the storage size.

Step 2: Get a domain name

Get a domain name and map your IP to the domain name. For free domain names, visit afraid dns. Create three sub-domain names and map the IP address of your server to them. The three domains are for the:

  • application

  • proxy server

  • database manager

Step 3: Create Dockerfiles for the Application

You can start by using the docker init to get a template for the application and adjust the commands to suit your needs or the requirements stated in the READme file of the frontend and backend directory.

You have to create separate docker files to build the application; one for the frontend and the other for the backend.

Frontend Dockerfile

# Use the latest official Node.js image as a base
FROM node:latest

# Set the working directory
WORKDIR /app

# Copy the application files
COPY . .

# arg command
ARG VITE_API_URL=${VITE_API_URL}

# Install dependencies
RUN npm install

# Expose the port the development server runs on
EXPOSE 5173

# Run the development server
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

Backend docker file

# Use the latest official Python image as a base
FROM python:latest

# Install Node.js and npm
RUN apt-get update && apt-get install -y \
    nodejs \
    npm

# Install Poetry using pip
RUN pip install poetry

# Set the working directory
WORKDIR /app

# Copy the application files
COPY . .

# Install dependencies using Poetry
RUN poetry install
ENV PYTHONPATH=/app

# Expose the port FastAPI runs on
EXPOSE 8000

# Run the prestart script and start the server
CMD ["sh", "-c", "poetry run bash ./prestart.sh && poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"]

Step 4: Update .env files

Go into the frontend and backend directory and update the .env files

.env file for the frontend

This file holds the domain name that is attached to your virtual machine IP address.

VITE_API_URL=https://sapphireaura.twilightparadox.com

.env file for the backend

# Domain
# This would be set to the production domain with an env var on deployment
DOMAIN=localhost

# Environment: local, staging, production
ENVIRONMENT=local

PROJECT_NAME="Full Stack FastAPI Project"
STACK_NAME=full-stack-fastapi-project

# Backend
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://<server-ip>:5173"
SECRET_KEY=2e2************************
FIRST_SUPERUSER=devops@hng.tech
FIRST_SUPERUSER_PASSWORD=devops#HNG11
USERS_OPEN_REGISTRATION=True
DATABASE_URLpostgresql://${POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}:5432/{POSTGRES_DB}
# Emails
SMTP_HOST=
SMTP_USER=
SMTP_PASSWORD=
EMAILS_FROM_EMAIL=info@example.com
SMTP_TLS=True
SMTP_SSL=False
SMTP_PORT=587

# Postgres
POSTGRES_SERVER=db-postgres   # name of your db service
POSTGRES_PORT=5432
POSTGRES_DB=apidb
POSTGRES_USER=app
POSTGRES_PASSWORD=app

Step 5: Create a compose.yml file

Create a compose.yml file to build up your three-tier architecture. In a docker-compose.yml file, services represent the containers that will be created in the application.

This file contains the configuration build for the frontend, backend, database, proxy server, and adminer.

Adminer is an application used to manage databases while, the proxy manager is a tool that helps manage and configure routing traffic, and load balancing, and enhances security in web applications.

For this project, the nginx proxy server was used.

services:
  backend:
    build:
      context: ./backend
      args:
         - VITE_API_URL=https://sapphireaura.twilightparadox.com
    container_name: fastapi_app
    ports:
      - "8000:8000"
    depends_on:
      - db-postgres
    env_file:
      - ./backend/.env

  frontend:
    build:
      context: ./frontend
    container_name: nodejs_app
    ports:
      - "5173:5173"
    env_file:
      - ./frontend/.env

  db-postgres:
    image: postgres:latest
    container_name: postgres_db
    ports:
      - "5432:5432"
    # volumes:
    #   - postgres_data:/var/lib/postgresql/data
    env_file:
        - ./backend/.env
  adminer:
    image: adminer
    container_name: adminer
    ports:
      - "8080:8080"
    depends_on:
      - db-postgres

  proxy:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx_proxy_manager
    restart: unless-stopped
    ports:

      - '80:80'    # Public HTTP Port
      - '8090:81'    # Admin Web Port
      - '443:443'  # Public HTTPS Port
    environment:
      DB_SQLITE_FILE: "/data/database.sqlite"
      DB_PGSQL_HOST: "db"
      DB_PGSQL_PORT: 5432
      DB_PGSQL_USER: "proxy_user"
      DB_PGSQL_PASSWORD: "proxy00123"
      DB_PGSQL_NAME: "proxy_db"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    depends_on:
      - db-postgres
      - backend
      - frontend
      - adminer

volumes:
  postgres_data:
  data:
  letsencrypt:

Step 6: Build the application

It is time to bring the app to life. Run the commands

$ docker-compose up -d

Step 7: Create your ngnix.conf file

This file is the main configuration file for the Nginx web server. It defines the behavior of the Nginx server, specifying how it should handle requests, route traffic, manage resources, and apply security settings.

Your goal in a three-tier app is to route from the front end to the back end. For this application, I decided to route to /dcos, /redoc and /api . The fastapp_api represents the backend service stated in the compose.yml.

In other words. I want the front-end service to route to my back-end service.

location /api {    
    proxy_pass http://fastapi_app:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

location /docs {
    proxy_pass http://fastapi_app:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

location /redoc {
    proxy_pass http://fastapi_app:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Step 8: Login to nginx proxy server

Copy the domain name of your proxy server and paste it into your browser.

Login with the credentials:

  • Email: admin@example.com

  • Password: changeme

Step 9: Request for SSL Cert

At this stage, you have to request an SSL certificate for each of your domain names. To do that:

  • click on SSL Certificates

  • click on Add SSL Certificates

  • click on Let's Encrypt

  • type your domain name.

  • click on Test server Reachability

  • click on the I Agree button

  • click on Save

Step 10: Configure your application's Routing

  1. Click on Hosts

  2. Click on the Proxy Host tab to add your configurations.

  3. Click on Add Proxy Host

  4. type the domain name you desire to use for the application

  5. type the container name of the front-end app

  6. type the port of the frontend port

  7. click on Advanced .

  8. paste the nginx.conf configuration in the input field.

  9. click Save

  10. View your application on the browser.

  11. Go through this process for the Adminer. You don't need a Nginx.conf file for that. Use the domain name assigned for the DB and configure using the Adminer container name and port.

  12. You can also go through these processes for your proxy if you want it to have an SSL cert.

Proof of Deployment

10
Subscribe to my newsletter

Read articles from Ugochi Ukaegbu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ugochi Ukaegbu
Ugochi Ukaegbu

DevOps/Cloud Engineer who loves learning, sharing knowledge and enjoys engaging with others on various topics. Welcome to my Universe of Learning, where I transform complex ideas into simple forms. My passion for sharing knowledge fuels my writing, making it accessible and fun.