Introduction
This stage brought on a task that at first glance seems easy and straightforward, but when the added requirements were introduced, the complexity grew and the challenge became harder. The task instructs us to containerize a three tier application on a single server and use a proxy manager like nginx to configure reverse proxying to ensure the frontend and backend can be served from the same port. That’s not all. It gets more complex.
Here are the full requirements for completing this tasks:
Ensure the application runs locally before writing Dockerfiles
Configure the Frontend and Backend to listen on port 80
Obtain a domain name for the project
Write Dockerfiles to containerize the frontend and backend
Install adminer to enable database manager through its GUI
Configure Nginx proxy manager to handle reverse proxying and setup SSL certificates
Let’s get started
Prerequisites
A virtual machine running Ubuntu
Basic Level Understanding of the Linux CLI
Step 1
Clone the repo
First we have to clone the repository from Github
cd devops–stage–2
Step 2
Configure the backend
The frontend of this application depends on the backend for full functionality so we will begin by configuring the backend.
Dependencies
The backend depends on a postgresQL database, It would also require poetry to be installed before starting up
Installing Poetry
To install Poetry, follow these steps:
Add Poetry to your PATH if it’s not automatically added:
export PATH=“$HOME/.poetry/bin:$PATH“ >> ~/.bashrc
source ~./bashrc
poetry —version
Replace $HOME/.poetry/bin with the appropriate path where Poetry binaries are installed if different on your system. This ensures you can run Poetry commands from any directory in your terminal session.
Install dependencies using Poetry:
Setup PostgreSQL:
Follow these steps to install PostgreSQL on Linux and configure a user named app with password my_password and a database named app. Give all permissions of the app database to the app user.
Install PostgreSQL on Linux (example for Ubuntu):
sudo apt install postgresql postgresql–contrib
Switch to the PostgreSQL user and access the PostgreSQL
psql
Create a user app with password my_password:
Create a database named app and grant all privileges to the app user:
c app
GRANT ALL PRIVILEGES ON DATABASE app TO app;
GRANT ALL PRIVILEGES ON SCHEMA public TO app;
Exit the PostgreSQL shell and switch back to your regular user.
exit
Set database credentials
Edit the PostgreSQL environment variables located in the .env file. Make sure the credentials match the database credentials you just created.
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=app
POSTGRES_USER=app
POSTGRES_PASSWORD=my_password
Set up the database with the necessary tables:
Run the backend server and make it accessible on all network interfaces:
Step 3
Configure the frontend
Open up a new terminal.
P.S. We can split the terminal session using tmux or run it as a system service, but to keep things fairly simple, we would leave the backend running in one terminal and open another terminal for the frontend.
Dependencies
The frontend was built with Nodejs and npm for dependency management.
sudo apt install nodejs npm
Install dependencies:
Run the fronted server and make it accessible from all network interfaces:
Accessing the application using curl:
Step 4
Accessing the UI
Open your browser and navigate to:
Enable login access from the UI:
The login credentials can be found in the .env located in the backend folder
FIRST_SUPERUSER=devops@hng.tech
FIRST_SUPERUSER_PASSWORD=devops#HNG11
If we try login in now we would be met with a network error.
Looking through the developer tools we can see that connecting to the backend on http://localhost:8000 was refused. This is because we are using a remote server and localhost in our browser’s context means our personal computer. So to properly route the browser to the remote server running the application. we will have to Change the VITE_API_URL variable in the frontend .env file:
If we try to login now we are met with a new error called CORS which stands for Cross-origin resource sharing.
Basically, our backend doesn’t recognise the origin of the request which is coming from our server’s IP, so we need to tell our backend to accept request coming from that particular IP address.
In our backed .env file we need to add http://<your_server_IP>:5173 to the end of the string of allowed IPs
Now If we try one more time to login.
We successfully setup the application locally.
We can also access the swagger API as well as the documentation paths using http://<your_server_IP>:8000/doc and http://<your_server_IP>:8000/redoc respectively.
Step 5
Containerizing the application
Now we need to repeat the entire process, but this time, We would utilize Docker containers. we will start by writing Dockerfiles for both frontend and backend and then move to the project’s root directory and configure a docker compose file that will run and configure:
The Frontend and Backend
The postgres database the backend depends on
Adminer
Nginx proxy Manager
Let’s start by writing the Dockerfile for the backend application
vim Dockerfile
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
# 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“]
This repeats the entire process we carried out locally all in one file.
Now let’s set up the frontend.
vim Dockerfile
FROM node:latest
# Set the working directory
WORKDIR /app
# Copy the application files
COPY . .
# Install dependencies
RUN npm install
# Expose the port the development server runs on
EXPOSE 5173
# Run the development server
CMD [“npm“, “run“, “dev“, “—“, “–host“]
Again, this simply repeats the process we carried out to run the frontend locally.
Step 6
Docker compose setup
Navigate to the project root directory and create a docker-compose.yml file
vim docker–compose.yml
Copy this configuration into it
services:
backend:
build:
context: ./backend
container_name: fastapi_app
ports:
– “8000:8000“
depends_on:
– db
env_file:
– ./backend/.env
frontend:
build:
context: ./frontend
container_name: nodejs_app
ports:
– “5173:5173“
env_file:
– ./frontend/.env
db:
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“
proxy:
image: jc21/nginx–proxy–manager:latest
container_name: nginx_proxy_manager
ports:
– “80:80“
– “443:443“
– “81:81“
environment:
DB_SQLITE_FILE: “/data/database.sqlite“
volumes:
– ./data:/data
– ./letsencrypt:/etc/letsencrypt
depends_on:
– db
– backend
– frontend
– adminer
volumes:
postgres_data:
data:
letsencrypt:
Breakdown of the docker-compose.yml File
Here’s an explanation of each section in the provided docker-compose.yml file:
Services
Services are the containers that make up the application. Each service runs one image and can define volumes and networks. Each container can connect to any container in the same network using the service name.
Backend Service
build:
context: ./backend
container_name: fastapi_app
ports:
– “8000:8000“
depends_on:
– db
environment:
POSTGRES_SERVER: ${POSTGRES_SERVER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
build.context: Specifies the build context, pointing to the ./backend directory which contains the Dockerfile for building the FastAPI backend service.
container_name: Sets the container name to fastapi_app.
ports: Maps port 8000 on the host to port 8000 in the container.
depends_on: Ensures the db service is started before the backend service.
environment: Injects environment variables from the .env file, used by the FastAPI application to connect to the PostgreSQL database.
Frontend Service
build:
context: ./frontend
container_name: nodejs_app
ports:
– “5173:5173“
environment:
VITE_API_URL: ${VITE_API_URL}
build.context: Points to the ./frontend directory for building the Node.js frontend service.
container_name: Names the container nodejs_app.
ports: Maps port 5173 on the host to port 5173 in the container.
environment: Injects the VITE_API_URL environment variable from the .env file, used by the frontend application to connect to the backend API.
Database Service
image: postgres:latest
container_name: postgres_db
ports:
– “5432:5432“
volumes:
– postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
image: Uses the latest PostgreSQL image from Docker Hub.
container_name: Names the container postgres_db.
ports: Maps port 5432 on the host to port 5432 in the container, which is the default port for PostgreSQL.
volumes: Mounts a Docker volume postgres_data to persist database data.
environment: Sets database-related environment variables from the .env file for initializing PostgreSQL.
Adminer Service
image: adminer
container_name: adminer
ports:
– “8080:8080“
image: Uses the Adminer image, a database management tool.
container_name: Names the container adminer.
ports: Maps port 8080 on the host to port 8080 in the container for accessing the Adminer web interface.
Proxy Service
image: jc21/nginx–proxy–manager:latest
container_name: nginx_proxy_manager
ports:
– “80:80“
– “443:443“
– “81:81“
environment:
DB_SQLITE_FILE: “/data/database.sqlite“
volumes:
– ./data:/data
– ./letsencrypt:/etc/letsencrypt
depends_on:
– db
– backend
– frontend
– adminer
image: Uses the latest Nginx Proxy Manager image.
container_name: Names the container nginx_proxy_manager.
ports: Maps ports 80, 443, and 81 on the host to the same ports in the container for HTTP, HTTPS, and the Nginx Proxy Manager admin interface.
environment: Sets the environment variable for the SQLite database location.
volumes: Mounts the data directory for storing proxy manager data and the letsencrypt directory for SSL certificates.
depends_on: Ensures the db, backend, frontend, and adminer services are started before the proxy service.
Volumes
postgres_data:
data:
letsencrypt:
Defines named volumes to persist data across container restarts.
Step 7
Domain Setup
We need to setup domains and subdomains for the frontend, adminer service and Nginx proxy manager.
Remember we are required to route port 80 to both frontend and backend:
domain – Frontend
domain/api – Backend
db.domain – Adminer
proxy.domain – Nginx proxy manager
If you don’t have a Domain name, you can acquire a subdomain at AfraidDNS. That’s where i acquired the domain I used for this project. Ensure you route all the required domains above to the server your application is running on.
Step 8
Routing domains using Nginx proxy manager
We now have everything set up, we can run docker-compose up -d to get our application up and running. We would need to install Docker and Docker-compose first.
Install Docker
Update the package list:
Install required packages:
apt–transport–https
ca–certificates
curl
software–properties–common
Add Docker’s official GPG key:
Add the Docker repository to APT sources:
“deb [arch=amd64] https://download.docker.com/linux/ubuntu
$(lsb_release -cs)
stable“
Update the package list again:
Install Docker:
Verify that Docker is installed correctly:
Install Docker Compose
Download the latest version of Docker Compose:
Apply executable permissions to the binary:
Verify that Docker Compose is installed correctly:
Post-Installation Steps for Docker
Manage Docker as a non-root user:
Create the docker group if it doesn’t already exist:
Add your user to the docker group:
Now we can start up the application.
Ensure you are in the project root directory
Start the application
If you get a permission denied error, run is as superuser
Running curl localhost gives us a HTML response that Nginx proxy manager is successfully installed
Step 9
Reverse Proxying and SSL setup with Nginx proxy manager
Access the Proxy manager UI by entering http://:81 in your browser, Ensure that port is open in your security group or firewall.
Login with the default Admin credentials
Email: admin@example.com
Password: changeme
Click on Proxy host and setup the proxy for your frontend and backend
Map your domain name to the service name of your frontend and the port the container is listening on Internally.
Click on the SSL tab and request a new certificate
Now to configure the frontend to route api requests to the backend on the same domain, Click on Advanced and paste this configuration
proxy_pass http://backend: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://backend: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://backend: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;
}
Repeat the same process for
db.domain: to route to your adminer service on port 8080
proxy.domain: to route to the proxy service UI on port 81
You don’t need to do the advanced setup on the db and proxy domain
Step 10
Setup Adminer
Access the adminer web interface on db.<your_domain>.com
Login with the db credentials in your backend .env file
Step 11
Setup Frontend Login
Access your frontend on <your_domain>
Before you login, make sure to change change the API_URL in your frontend .env to the name of your domain
You would need to run docker-compose up -d –build to enable the changes to take effect
Your login should be successful now
Conclusion
We have now successfully:
Configured and tested the full stack application locally
Containerized the application
Setup Docker compose
Configured Adminer for Database management
Configured Reverse Proxying with Nginx Proxy Manager
Setup SSL certificates for our domains
Thank you for reading ♥
Happy Proxying! 🚀