hey folks, if you want to host your website or are already hosted on a virtual machine and are tired to push-pull code every time this blog post is for you where you learn about GitHub actions and how we can use it to automate the process once you push the code that Github action automatically Pull the code on Virtual machine and restart the services.
What is GitHub Actions?
GitHub Actions is a CI/CD (Continuous Integration and Continuous Deployment) tool offered by GitHub. It enables you to automate your software development workflows directly in your GitHub repository. With Actions, you can create workflows that perform tasks based on specific events—such as pushing code to a repository, creating pull requests, or scheduling tasks at specific times.
In the context of a Django project, GitHub Actions can be used to automate tasks such as running tests, building your application, and deploying code to your virtual machine (VM). This way, you can keep your website or app up-to-date without manually logging into the server and pulling the latest code every time you make a change.
Setting Up GitHub Actions for Django Deployment
Let’s set up a basic workflow to automate the deployment of your Django project to your virtual machine.
Step 1: Create a GitHub Actions Workflow
In your Django project repository on GitHub, go to Actions and click on New workflow. GitHub offers several templates, but for now, let’s create a custom workflow. Follow these steps:
- Create a
.github/workflows
directory in the root of your repository if it doesn't already exist. - Inside this directory, create a new file named
deploy.yml
(or any name you prefer).
2. Workflow Name and Trigger Event
The name
defines the workflow’s name, and the on
section specifies when it will run. Here, the workflow runs whenever code is pushed to the main
branch.
name: Django Deployment on VM
on:
push:
branches:
- main # Specify the branch to deploy, e.g., "main"
This part of the workflow listens for pushes to the main
branch, which will automatically trigger the deployment process whenever you update this branch.
3. Define the Job and Virtual Environment
The jobs
section specifies what the workflow will do. Here, we define a job named deploy
, which runs on an ubuntu-latest
GitHub-hosted virtual environment.
jobs:
deploy:
runs-on: ubuntu-latest # Runs on a GitHub-hosted Ubuntu server
This line means our job will run on the latest version of Ubuntu, which is often compatible with most Django project requirements.
4. Checkout the Code
The checkout step pulls the latest code from the GitHub repository so that we can work with it in the virtual environment.
steps:
- name: Checkout code
uses: actions/checkout@v3 # Checks out your code
Using actions/checkout@v3
, we access the repository files and ensure they’re ready for the next steps.
5. Set Up SSH Key
To securely connect to the VM, we add a private SSH key as an environment variable (SSH_PRIVATE_KEY
). The workflow creates an .ssh
directory, saves the SSH key in ~/.ssh/id_rsa
, and sets the necessary permissions. The StrictHostKeyChecking no
setting bypasses prompts about host authenticity.
- name: Setup SSH key
env:
SSH_PRIVATE_KEY: ${{ secrets.DO_SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "StrictHostKeyChecking no" >> ~/.ssh/config
6. Add Known Hosts
Here, we add the VM’s IP address to the known_hosts
file, allowing the GitHub Actions runner to verify the host's authenticity on SSH.
- name: Add Known Hosts
run: |
ssh-keyscan -H ${{ secrets.DO_SERVER_IP }} >> ~/.ssh/known_hosts
7. Test SSH Connection
This step tests the SSH connection to ensure the VM is accessible before deployment. It uses the SSH command to print "SSH connection successful" if the connection is valid.
- name: Test SSH Connection
run: |
echo "Testing SSH connection..."
ssh -v root@${{ secrets.DO_SERVER_IP }} 'echo "SSH connection successful"'
env:
DO_SERVER_IP: ${{ secrets.DO_SERVER_IP }}
8. Deploy to VM
This step initiates the actual deployment. It runs commands on the VM using SSH, updating the project and restarting services as needed.
- name: Deploy to VM
env:
DO_SERVER_IP: ${{ secrets.DO_SERVER_IP }}
Let’s break down each part inside this Deploy to VM
step:
Connect to VM and Check for Project Directory
- Check if Directory Exists: If the directory
/path/to/your/project
doesn’t exist, it creates it. - Initialize Git Repository: Inside the directory, it initializes a git repository and adds the remote URL (replace
YOUR_USERNAME
andYOUR_REPO
with your GitHub information).
ssh root@$DO_SERVER_IP << 'ENDSSH'
set -e
echo "Starting deployment..."
# Check if directory exists
if [ ! -d "/path/to/your/project" ]; then
echo "Creating directory..."
mkdir -p /path/to/your/project
cd /path/to/your/project
git init
git remote add origin <https://github.com/YOUR_USERNAME/YOUR_REPO.git>
else
cd /path/to/your/project
fi
Fetch Latest Changes and Check for Updates
- Fetch Changes: Fetches the latest code from the
main
branch. - Check for Changes: If there are updates in the branch, it pulls the latest code.
# Fetch latest changes
echo "Fetching latest changes..."
git fetch origin main || {
echo "Failed to fetch from origin"
exit 1
}
if ! git diff --quiet HEAD..origin/main 2>/dev/null; then
echo "Changes detected, updating code..."
git pull origin main
Activate Virtual Environment and Install Dependencies
- Activate Virtual Environment: Checks if the virtual environment (
venv
) exists and activates it. - Install Dependencies: Installs any new or updated dependencies from
requirements.txt
.
# Activate virtual environment if it exists
if [ -f "venv/bin/activate" ]; then
source venv/bin/activate
pip install -r requirements.txt
fi
Run Django Commands
- Apply Migrations: If
manage.py
exists, runs Django migrations to update the database. - Collect Static Files: Gather static files in a central location for easy access in production.
# Run Django commands if manage.py exists
if [ -f "manage.py" ]; then
python manage.py migrate --noinput
python manage.py collectstatic --noinput
fi
Restart Services
- Restart Gunicorn: Checks if Gunicorn (Django’s application server) is active and restarts it.
- Restart Nginx: Similarly, checks if Nginx (the web server) is active and restarts it.
echo "Restarting services..."
# Restart Gunicorn
if systemctl is-active --quiet gunicorn; then
sudo systemctl restart gunicorn || {
echo "Failed to restart Gunicorn"
exit 1
}
echo "Gunicorn restarted successfully"
else
echo "Warning: Gunicorn service not active"
fi
# Restart Nginx
if systemctl is-active --quiet nginx; then
sudo systemctl restart nginx || {
echo "Failed to restart Nginx"
exit 1
}
echo "Nginx restarted successfully"
else
echo "Warning: Nginx service not active"
fi
echo "Deployment completed successfully"
else
echo "No changes detected. Skipping deployment."
fi
ENDSSH
If no changes are detected, the script skips the deployment, saving time and resources.
Complete workflow file:
name: Django Deployment on VM
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup SSH key
env:
SSH_PRIVATE_KEY: ${{ secrets.DO_SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "StrictHostKeyChecking no" >> ~/.ssh/config
- name: Add Known Hosts
run: |
ssh-keyscan -H ${{ secrets.DO_SERVER_IP }} >> ~/.ssh/known_hosts
- name: Test SSH Connection
run: |
echo "Testing SSH connection..."
ssh -v root@${{ secrets.DO_SERVER_IP }} 'echo "SSH connection successful"'
env:
DO_SERVER_IP: ${{ secrets.DO_SERVER_IP }}
- name: Deploy to VM
env:
DO_SERVER_IP: ${{ secrets.DO_SERVER_IP }}
run: |
ssh root@$DO_SERVER_IP << 'ENDSSH'
set -e
echo "Starting deployment..."
# Check if directory exists
if [ ! -d "/path/to/your/projectz" ]; then
echo "Creating directory..."
mkdir -p /path/to/your/project
cd /path/to/your/project
git init
git remote add origin <https://github.com/YOUR_USERNAME/YOUR_REPO.git>
else
cd /path/to/your/project
fi
# Fetch latest changes
echo "Fetching latest changes..."
git fetch origin main || {
echo "Failed to fetch from origin"
exit 1
}
if ! git diff --quiet HEAD..origin/main 2>/dev/null; then
echo "Changes detected, updating code..."
git pull origin main
# Activate virtual environment if it exists
if [ -f "venv/bin/activate" ]; then
source venv/bin/activate
pip install -r requirements.txt
fi
# Run Django commands if manage.py exists
if [ -f "manage.py" ]; then
python manage.py migrate --noinput
python manage.py collectstatic --noinput
fi
echo "Restarting services..."
# Restart Gunicorn
if systemctl is-active --quiet gunicorn; then
sudo systemctl restart gunicorn || {
echo "Failed to restart Gunicorn"
exit 1
}
echo "Gunicorn restarted successfully"
else
echo "Warning: Gunicorn service not active"
fi
# Restart Nginx
if systemctl is-active --quiet nginx; then
sudo systemctl restart nginx || {
echo "Failed to restart Nginx"
exit 1
}
echo "Nginx restarted successfully"
else
echo "Warning: Nginx service not active"
fi
echo "Deployment completed successfully"
else
echo "No changes detected. Skipping deployment."
fi
ENDSSH
Final Step: Add Your Secrets
To securely connect to your virtual machine, add an SSH key to your repository secrets:
- In your GitHub repository, go to Settings > Secrets > Actions.
- Click New Repository secret.
- Add a name (e.g.,
SSH_KEY
) and paste the content of your private SSH key.
like here I use do_server_ip and do_ssh_private_key (make sure you want to copy the complete private key )
-----BEGIN OPENSSH PRIVATE KEY-----
[key content]
-----END OPENSSH PRIVATE KEY-----
The "server_ip" refers to your virtual machine's IP address. It's similar to when you run the SSH command locally using "ssh username@host" — in this case, we use the host (IP address) for the connection.
Test Your Workflow
Push a change to your main
branch (or whichever branch you specify). GitHub Actions should now trigger the workflow, deploy your changes, and restart your Django services on your virtual machine!