A Dockerized app that views, creates and deletes movie reviews.
- Using Python, build a working REST API backend service that manages (CRUD) movie reviews.
- Containerize the app and the PostgreSQL with Docker.
- Using Terraform, provision an AWS EC2 infrastructure where the app containers are deployed and run.
- CI/CD with GitHub Actions - validate build integrity on each push.
Client
→ FastAPI (Docker)
→ PostgreSQL (Docker)
→ GitHub Actions (CI)
→ Terraform (Infrastructure)
→ AWS (EC2)
→ [Optional] Ansible (setup VM)
- Python
- Docker Desktop
- Terraform (and add to Path)
- AWS CLI v2
Create Python virtual environment, activate it and install libraries.
python -m venv venv
venv\Scripts\activate
pip install fastapi uvicorn[Optional]
Using Ansible to automate the AWS EC2 VM setup and app deployment:
Get a Linux VM, either WSL or VirtualBox, and install Ansible on it. The Linux machine acts as the Ansible control node.
Installation guide: https://docs.ansible.com/projects/ansible/latest/installation_guide/installation_distros.html
Create an app folder in the root directory, and create main.py and requirements.txt inside it.
- Import
FastAPI - Create an app using
FastAPI - Route the
/endpoint to a function that returns a basic object withmessagekey. - Route another endpoint -
/healthto a function that returns{ "status": "healthy" }.
So far, we only need to add these into this txt file.
fastapi
uvicorn
uvicorn app.main:app --reloaduvicorn- the server programapp.main- app/main.py:app- the variable namedappin the file--reload- automatically restart server when code changes
Go to http://127.0.0.1:8000, observe that the message is shown.
Additionally, go to http://127.0.0.1:8000/docs to view the documentations for FastAPI.
We will now create the CRUD - Create, Read, Update and Delete endpoints and test them before moving on to the real database - PostgreSQL.
Still inside app folder, create a new Python file - schemas.py.
Import BaseModel from pydantic. Pydantic is used internally by FastAPI. BaseModel allows us to define structured data, validate incoming requests, and automatically generate API docs.
Create a blueprint named Review using the class keyword which contains:
- movie (string)
- rating (integer)
- comment (string)
Import the blueprint into main.py.
Create a blank list as the temporary data storage for reviews, just for testing.
Create a route path /reviews with these endpoints:
POST: [Create] creates a review by appending it into the list, where the function's input argument is ofReviewtype.GET: [Read] returns all reviews from the list.PUT: [Update] replaces the review of index {id} with the input review. Input arguments:id (int),review (Review).DELETE: [Delete] deletes a review from the list. It returns a simple object containing a message to indicate that the delete is successful.
Go to http://127.0.0.1:8000/docs > Try it out of each endpoint to try creating/adding, viewing and deleting reviews to test and confirm that all those endpoints work correctly.
Example of console outputs observed from running the CRUD requests:
INFO: 127.0.0.1:46648 - "GET /docs HTTP/1.1" 200 OK
INFO: 127.0.0.1:46648 - "GET /openapi.json HTTP/1.1" 200 OK
INFO: 127.0.0.1:2006 - "GET /reviews HTTP/1.1" 200 OK
INFO: 127.0.0.1:1239 - "POST /reviews HTTP/1.1" 200 OK
INFO: 127.0.0.1:56615 - "POST /reviews HTTP/1.1" 200 OK
INFO: 127.0.0.1:57779 - "POST /reviews HTTP/1.1" 200 OK
INFO: 127.0.0.1:41291 - "GET /health HTTP/1.1" 200 OK
INFO: 127.0.0.1:13824 - "GET /reviews HTTP/1.1" 200 OK
INFO: 127.0.0.1:3492 - "PUT /reviews?id=1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:3492 - "GET /reviews HTTP/1.1" 200 OK
INFO: 127.0.0.1:16935 - "DELETE /reviews/2 HTTP/1.1" 200 OK
INFO: 127.0.0.1:11580 - "GET /reviews HTTP/1.1" 200 OK
Install DB dependencies for Python.
pip install sqlalchemy psycopg2-binaryUpdate requirements.txt
fastapi
uvicorn
sqlalchemy
psycopg2-binary
In app folder, create database.py as the database connection file.
- Import
create_enginefromsqlalchemy. - Import
sessionmakerfromormofsqlalchemy.- ORM: Object Relational Mapper
- Store the database URL -
postgresql://postgres:password@localhost:5432/moviesinto a string. - Use the URL as input argument to create an engine, stored in a variable. An engine is a connection to the database.
- Create a local session stored in a variable named
SessionLocal, using these parameters:bind: the engine. Means "use this engine".autoflush: False. Do not automatically flush pending changes to the database before queries.autocommit: False. Transactions must be explicitly committed to persist changes in PostgreSQL.
In app folder, create models.py that creates and defines the database table.
- Import
Column,IntegerandStringfromsqlalchemy.- Column: Represents a column in a database table
- Integer, String: Data types in the database table
- Import
declarative_basefromormofsqlalchemy. It is the foundation to create ORM models, which allows us to define database tables as Python classes. - Import the database engine from
database.py. - Create a declarative base and store it in a variable named
Base. - We will now create the blueprint of
Reviewhere the SQLAlchemy way instead. - Using
Baseas the input argument, create theReviewclass.- Give it a table name, defined as
__tablename__. - Use
Column,IntegerandStringto create these columns in the table:- id (it's the primary key and also the index)
- movie (movie name)
- rating (integer)
- comment
- Give it a table name, defined as
- Finally, add this line at the end of the file:
Base.metadata.create_all(bind=engine). This is to use the engine to create all the database tables defined, if they don't already exist.
Now, we need to have PostgreSQL running. It's good to build and run it on Docker because it's cleaner, portable, avoids local installation mess, and it's more aligned with DevOps mindset.
In this project's root directory, create a file named docker-compose.yml.
- Create a service named
db. - Name the container as
postgres-db. - Use the official
postgresimage of PostgreSQL. - Specify the
POSTGRES_USER,POSTGRES_PASSWORDandPOSTGRES_DBunderenvironmentas the environment variables. Notice that they align with theDATABASE_URLdefined indatabase.py.- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: password
- POSTGRES_DB: movies
- Use
5432:5432as the port ofdb.
Make sure that Docker Desktop is running.
Use this command to run the PostgreSQL container:
docker compose up -dThen, we can either use command docker ps or use Docker Desktop to verify that the container is running.
Now, we should modify main.py such that the FastAPI routes to the real PostgreSQL database,
-
Delete
reviewslist. -
Import
SessionLocalfromdatabase.py. This allows the app to create PostgreSQL database sessions. -
Import
models.py. This imports the ORM models that represent the database tables. -
In each CRUD function, start by creating a session and store it in a variable named
dbfor use.db = SessionLocal()
-
Modify the
POSTfunction: Create a row in thereviewstable while passing thereviewobject data into it. Add the row into the db. Commit the changes, and refresh (reload) the row in Python.db_review = models.Review( movie=review.movie, rating=review.rating, comment=review.comment ) db.add(db_review) db.commit() db.refresh(db_review) return db_review
models.Review(): Create a Python ORM object that represents a row in the database tabledb.add(): Insert data into the ORM objectdb.commit(): Permanently save the changes into PostgreSQLdb.refresh(): Sync/Update the objectdb_reviewwith the latest data from PostgreSQLreturn db_review: Return the inserted/added review
-
Note: The
Reviewimported fromschema.pyis the data type of each review, whereas theReviewimported frommodels.pyis the database tablereviews. -
Modify the
GETfunction:return db.query(models.Review).all()
The SQL equivalent of it is:
SELECT * FROM reviews. -
Modify the
PUTfunction: Query the database to look for the row of the review with the given ID. Modify each key of that row. Commit the changes and refresh the row in Python.db_review = db.query(models.Review).filter(models.Review.id == id).first() db_review.movie = review.movie db_review.rating = review.rating db_review.comment = review.comment db.commit() db.refresh(db_review) return db_review
The query is equivalent to this SQL:
SELECT * FROM reviews WHERE id = ? LIMIT 1; -
Modify the
DELETEfunction: Use the exact same query to find the target row. Then, delete the row, and commit the changes. No refresh needed because the row has already been deleted. Finally, return a message to indicate that the review of that ID has been deleted.db_review = db.query(models.Review).filter(models.Review.id == id).first() db.delete(db_review) db.commit() return { "message": f"Review of ID {id} has been deleted." }
In this project's root directory, create a file named Dockerfile.
The Dockerfile defines the steps to build a Docker image for the app.
These are the commands/instructions/directives that will be used in the Dockerfile of this project:
- FROM
- WORKDIR
- COPY
- RUN
- EXPOSE
- CMD
Study each of them, and use them to write commands that carry out these steps:
- Use Python version 3.12.
- Use
/appas the folder where the app runs, acting as the 'current directory'. - Copy the dependencies file -
requirements.txtinto the current directory of the container. pip installall the dependencies using thetxtfile.- Copy everything under the
appfolder into the/app/appfolder of the container.- In the container,
/appis where the project resides. - So, the actual
/appfolder in the container will then be located as/app/app.
- In the container,
- Let the app use port 8000.
- Run the
uvicorncommand to start the app on0.0.0.0port8000.
Question: Why don't we copy everything into the container first before running pip install?
Answer:
If we do so, then as long as any of the project's content is modified but requirements.txt has no change, pip install will still be re-run unnecessarily, causing waste of time and resources.
This is because Docker builds in layers (steps) and cache. The numbering of steps shown above are layers. If a layer has changes, then that layer and all those after it will be rebuilt. Meanwhile, the layers before are not, as they are cached and reused.
Therefore, by doing so, pip install only runs when the dependencies change. This makes builds faster.
First of all, we must know that Docker containers communicate with one another via service names.
This is because in the Compose network, a service name is actually the DNS hostname, which points to the container's IP automatically.
We've already created and defined a service - db in the docker-compose.yml, which is the PostgreSQL database.
Now, we will create and define another service in there for our app instead, which is just called app, and we'll name the container fastapi-app.
Therefore, we'll end up with 2 services. So, for app to talk to db, the database URL specified in database.py of app must be modified. Instead of localhost, it must be changed to the service name of the database container - db.
DATABASE_URL = "postgresql://postgres:password@db:5432/movies"Now we can update docker-compose.yml.
- Add healthcheck to
db.healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 10
- Create a service named
app. - Specify where the
Dockerfileis located relative to this.ymlfile. So it reads theDockerfileto build and create the image. - Name the container as
fastapi-app. - Use port
8000:8000. - Specify that it depends on the service
db.condition: service_healthy
With the healthcheck on db and app depending on db's health before it starts, we fixed the problem where FastAPI starts before PostgreSQL is ready, which causes build errors.
Question: When to use - prefix in docker-compose?
Answer: The - prefix indicates a sequence (ordered list of values) in YAML.
Now, run this command to build and start the containers:
docker compose up --buildIf you wish to stop and delete the containers before rebuilding, run:
docker compose downQuestion: What's the difference between docker compose up -d and docker compose up --build?
Answer:
| Command | Build image | Runs in background | Shows logs |
|---|---|---|---|
up --build |
✅ | ❌ | ✅ |
up -d |
❌ | ✅ | ❌ |
Personally I prefer combining both options: docker compose --build -d, but this doesn't show the logs.
For this project, we'll introduce an automated CI/CD that runs during each push to its GitHub repository to check the software integrity. It builds the Docker image and checks whether it's successful.
push to GitHub
↓
GitHub Actions runner starts
↓
checkout project repository
↓
build Docker image
↓
check running containers
↓
test FastAPI endpoint
↓
(success/fail)
Study and refer to this page for the documentations of GitHub Actions - https://docs.github.com/en/actions
First of all, create a folder in the root directory named .github.
Inside it, create a folder named workflows.
Go inside, create a file named docker-build.yml.
.github/
└── workflows/
└── docker-build.yml
Write the .yml file to build the workflow shown above.
- Name:
docker-build - Run name:
Build Docker image and test FastAPI endpoint - Job name:
build-docker-image - Runs on:
ubuntu-latest
Give a name to each step.
steps:
- name: ...
run: ...Use paths to only watch changes from some files/folders.
on:
push:
paths:
- 'app/**'Use a simple bash snippet to test the FastAPI endpoint using a loop and pause intervals. Exit with code 0 if it starts properly in time. Otherwise, exit with code 1 (error). Use this command to test the endpoint:
curl -s http://localhost:8000/docs | grep "Swagger UI"In this stage, we'll be setting up our AWS cloud infrastructure. We'll use the infrastructure to host and run our Docker containers. Meanwhile, make sure you have installed AWS CLI.
aws --version- Register for an AWS user account here - https://signin.aws.amazon.com/signup?request_type=register.
- Go to https://console.aws.amazon.com/ and sign in using root user email.
IAM stands for Identity and Access Management.
- In the console, use the search bar at the top to search for "IAM", and click on "IAM - Manage access to AWS resources".
- From the navigation bar on the left, go to
Access Management>IAM user groups, and then click onCreate group. This is a good practice, as instead of assigning permissions directly to users, we use a group. - Name the group as
devops-admin, and attach the policyAdministratorAccess. Scroll down, and clickCreate user group.
- Now, go to
Access Management>IAM usersand click onCreate user. - Name the user as
terraform-user, and tickProvide user access to the AWS Management Console. - You can either autogenerate the password, or create a custom one. Just don't forget/lose it.
- As this is a learning project, we can untick this -
Users must create a new password at next sign-in. - Go to the next step -
Set permissions. UnderPermissions options, pickAdd user to group. - Add the user to the
devops-admingroup that we created. - Go to the next step -
Review and create. Review every info. If all is good, click onCreate user.
- Once again, go to
Access Management>IAM userswhere we can see the user that we just created. - On the user, click on
Create access key. - On the first step -
Access key best practices & alternatives>Use cases, pickCommand Line Interface (CLI). - Skip the description tag, and then click
Create access key. Then, download the.csvfile immediately. It containsAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY. Keep them well.
- Now, in
cmd, run this command:aws configure, and enter the AWS access keys as prompted. - Next, for the region, get it from the console page (https://console.aws.amazon.com/) top right corner, next to the gear ⚙️ icon. For example,
us-east-1for North Virginia, andus-west-2for Oregon. - Set
jsonas the default output format. - Now, run
aws sts get-caller-identityin the CMD to check the setup. If the account ID and the ARN is shown, the IAM setup is correct.
As of now, our project runs on the local PC.
Local machine → Docker Compose → FastAPI + PostgreSQL
Now, we will use Terraform and GitHub Actions to provision infrastructure and deploy the app automatically on AWS EC2.
GitHub Actions → Terraform → AWS EC2 (remote server) → FastAPI + PostgreSQL
In the root directory, create a folder named terraform, and inside it, create a file named main.tf as a start.
terraform/
└── main.tf
This is the plan that we'll build in main.tf.
Terraform
├── EC2 instance
├── Security group
│ ├── port 22 (SSH)
│ └── port 8000 (FastAPI)
└── key pair attachment
Open main.tf. We'll work on it step by step.
-
Download AWS plugin.
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } -
Add AWS provider
provider "aws" { region = "ap-southeast-1" } -
On CMD,
cdintoterraformfolder, and runterraform init. What this does:- Downloads AWS provider plugin
- Creates
.terraform/folder - Prepares the project for AWS operations
-
Now, try running
terraform planand observe the output. We'll most likely getNo changesorNo resources defined, because we haven't created anything yet.
In a .tf file, an AWS EC2 instance is known as a resource. Hence, we use the keyword resource to create and configure it.
-
We will now create the first AWS EC2 resource. First, go to AWS Console, search for
EC2and click into it. -
Now, we need to get one information from AWS Console - the AMI ID of Ubuntu server, which we need to specify in
main.tf. -
Go to AWS Console, search for
EC2and click into it. -
Go to
Dashboard>Launch instance. Note that we do NOT need to configure and launch the virtual server instance ourselves, because that's exactly what we need Terraform for - to provision (supply) it for us! -
Under
Application and OS Images, pick Ubuntu and copy its AMI ID. -
Scroll down, and note the
Instance typeas well. We will also specify it inmain.tf. For this project, we will just uset3.micro. It's a small AWS free-tier server that's good for learning. -
Return to
main.tf. Add this snippet into it. Notice that we name the server asfastapi-dev-serverundertags.resource "aws_instance" "fastapi-server" { ami = "ami-02dd44faa40720bb8" instance_type = "t3.micro" tags = { Name = "fastapi-dev-server" } } -
Now, on CMD, run
terraform planagain, and we'll see what Terraform will do. -
After that, to perform the actions to create the actual EC2 instance, run
terraform applyand typeyesto confirm. -
Awesome! Now, to see the server instance, just go to the AWS Console EC2 page >
Dashboard>Resources>Instances (Running)and click into it. Observe and confirm thatfastapi-dev-serveris up and running.
Terraform
├── EC2 instance ✅
├── Security group
│ ├── port 22 (SSH)
│ └── port 8000 (FastAPI)
└── key pair attachment
What happened actually?
We setup AWS CLI, which links to our AWS console.
Then, Terraform actually uses it to provision/create the EC2 server on our AWS console.
So, what we'll do next, is to deploy our FastAPI app and PostgreSQL containers into the server.
We need to be able to SSH into our EC2 server to install Docker and deploy our app containers.
Since we want to follow the mindset of Infrastructure as Code (IaC), let's use Terraform to create and attach the key pair for us, instead of manually creating it from AWS console.
First, create a tls_private_key resource in main.tf to generate SSH key pair using RSA.
resource "tls_private_key" "fastapi_key" {
algorithm = "RSA"
rsa_bits = 4096
}
Create an aws_key_pair resource named fastapi_key that points to the OpenSSH public key created by tls_private_key.
resource "aws_key_pair" "fastapi_key" {
key_name = "fastapi-dev-key"
public_key = tls_private_key.fastapi_key.public_key_openssh
}
Then, attach the key to the EC2 instance by adding this line into it.
key_name = aws_key_pair.fastapi_key.key_name
Finally, export the private key .pem into terraform folder. We'll use this .pem file to SSH into our server later.
resource "local_file" "private_key" {
content = tls_private_key.fastapi_key.private_key_pem
filename = "${path.module}/fastapi-dev-key.pem"
}
Terraform
├── EC2 instance ✅
├── Security group
│ ├── port 22 (SSH)
│ └── port 8000 (FastAPI)
└── key pair attachment ✅
We will now add another resource - AWS security group.
resource "aws_security_group" "fastapi-sg" {
name = "fastapi-security-group"
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "FastAPI"
from_port = 8000
to_port = 8000
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "fastapi-security-group"
}
}
Explanation:
ingress: Inbound traffic allowed into the server.- We open 2 ports - 22 and 8000 here, for SSH and FastAPI respectively.
egress: Outbound traffic from server.- This basically means that the server can access the internet.
- This is needed for
apt install,docker pull, etc.
Next, attach this security group resource to the aws_instance resource, by adding this line into it:
vpc_security_group_ids = [aws_security_group.fastapi-sg.id]
[Optional] Add an output in main.tf to show the public IP of the aws_instance.
output "ec2_public_ip" {
value = aws_instance.fastapi-server.public_ip
}
Skip this step if you are not following the automated setup in Chapter 9.1, which is using Ansible. Though, it's highly recommended.
First of all, create a folder named ansible in the root directory of this project.
Create another local file resource in main.tf, named "ansible_inventory".
resource "local_file" "ansible_inventory" {
filename = "${path.module}/../ansible/inventory.ini"
content = <<EOF
[fastapi_servers]
fastapi-dev-server ansible_host=${aws_instance.fastapi-server.public_ip} ansible_user=ubuntu ansible_private_key_file=~/.ssh/fastapi-dev-key.pem
EOF
}
This writes the following content into inventory.ini of ansible folder. If it doesn't exist, it will create it. Otherwise, it overwrites it.
[fastapi_servers]
fastapi-dev-server ansible_host=<public IP> ansible_user=ubuntu ansible_private_key_file=~/.ssh/fastapi-dev-key.pemNow that we've completed the 'Code' part in Infrastructure as Code (IaC), let's run it so it automatically provisions the AWS instance for us, without us needing to do everything manually on the AWS Console.
Run terraform apply to apply all the changes.
Go to AWS console > EC2 > Instances to observe and verify the changes.
Terraform
├── EC2 instance ✅
├── Security group
│ ├── port 22 (SSH) ✅
│ └── port 8000 (FastAPI) ✅
└── key pair attachment ✅
Ansible is another great tool in IaC. With an Ansible playbook, we can automate the setup process of our remote Ubuntu server that we just provisioned on AWS EC2.
Note that this section will continue to work in this project's workspace. We do not create a different project, but rather, make use of the ansible folder created in Chapter 8.5.
Copy the terraform/fastapi-dev-key.pem file into the ~/.ssh/ folder of whatever Linux machine that you're running Ansible from.
In Chapter 8.5, we already created the ansible folder and the inventory INI file. Next, we'll create the playbook - setup.yml.
---
- name: Configure AWS EC2 Instance and Deploy FastAPI App
hosts: fastapi_servers
pre_tasks:
- name: Update apt cache
become: yes
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 86400
roles:
- base
- docker
- fastapiThe cache valid time is 1 day in seconds.
Use ansible-galaxy to create these roles:
- base - update apt cache and install essential packages
- docker - install Docker and disable sudo for Docker
- fastapi - clone and run the app
ansible-galaxy role init <role>This will create a directory for each role.
Now, in each role's folder, go to tasks/main.yml.
-
base
--- # tasks file for base - name: Install core packages become: yes ansible.builtin.apt: name: - git state: present
This installs the
gitpackage on the Ubuntu VM. -
docker
--- # tasks file for docker - name: Install Docker become: yes ansible.builtin.apt: name: - docker.io - docker-compose-v2 state: present - name: Disable sudo requirement become: yes ansible.builtin.user: name: "{{ ansible_user_id }}" groups: docker append: yes - name: Reset connection to apply Docker group changes ansible.builtin.meta: reset_connection
This installs
dockeranddocker-compose, enablesubuntuuser to rundockercommand directly without needing tosudo, by addingubuntuuser to thedockergroup. -
fastapi
--- # tasks file for fastapi - name: Clone the FastAPI repository ansible.builtin.git: repo: "https://github.com/antonblaise/dockerized-fastapi-devops.git" dest: "{{ fastapi_repo_path }}" version: main - name: Build and run the containers become: yes ansible.builtin.command: cmd: docker compose up -d --build chdir: "{{ fastapi_repo_path }}" register: compose_result failed_when: - compose_result.rc != 0 - '"Already up-to-date" not in compose_result.stderr'
This clones the repository, builds and runs the app containers. Next, we need to define the
fastapi_repo_pathvariable invars/main.yml.--- # vars file for fastapi fastapi_repo_path: "{{ ansible_env.HOME }}/dockerized-fastapi-devops"
Now, we have our AWS EC2 instance of Ubuntu VM running our app images on the cloud!
Feel free to manually ssh into it and run docker ps or any other things you want, to see and verify the changes. Meanwhile, go to http://<public IP of server>:8000/docs on browser to test it out. Be sure to specify http for the address, or the page will be unreachable.
Since we now have our very own remote Ubuntu server, let's SSH into it.
On CMD, make sure you're in terraform folder where we exported the .pem file. Then, use the .pem file and the server's public IPv4 address to SSH into it. Username is ubuntu.
ssh -i fastapi-dev-key.pem ubuntu@<public IP of server>Update and upgrade its packages.
sudo apt update
sudp apt upgrade -y[Optional but recommended] Remove unnecessary packages and clean cache.
sudo apt autoremove -y
sudo apt autocleanInstall Docker, start Docker service and enable it on boot.
sudo apt install docker.io -y
sudo systemctl start docker
sudo systemctl enable dockerVerify Docker installation.
docker --versionNow, for every Docker command, we must sudo, which is definitely undesirable. So, we need to fix this permission issue by adding the current user into the Docker group.
sudo usermod -aG docker $USERNow, exit the server and SSH back in. Try this command. If there's no permission error, the Docker setup is complete.
docker psOn Ubuntu, Docker Compose is separate from Docker Engine. Also, it is not the same as the old docker-compose binary.
Run these to install it and verify the installation.
sudo apt install docker-compose-v2 -y
docker compose versionDo note that we'll be using docker compose instead of docker-compose here in Ubuntu. No hyphen -.
Run these to install and verify Git.
sudo apt install git -y
git --versionThen, clone this project's repository and cd into it.
git clone https://github.com/antonblaise/dockerized-fastapi-devops.git
cd dockerized-fastapi-devops/Use this command to build and run the containers.
docker compose up --build -dUse docker ps to check the containers. Meanwhile, go to http://<public IP of server>:8000/docs on browser to test it out. Be sure to specify http for the address, or the page will be unreachable.