mirror of
https://github.com/thecyberlearn/hostinger-django-demo.git
synced 2026-08-18 15:12:57 +00:00
🚀 TRANSFORM: Django VPS Deployment Toolkit
🔄 Complete transformation from mixed demo project to pure deployment toolkit REMOVED: ❌ Django demo project (core/, demo_project/, manage.py, requirements.txt) ❌ Demo-specific files and configurations ❌ Mixed-purpose confusion RESTRUCTURED: ✅ deploy-django-project.sh - Universal Django deployment ✅ setup-django-user.sh - VPS user setup ✅ setup-multi-webhook.sh - Auto-deploy webhooks ✅ webhook-router.py - Multi-project webhook handler ✅ templates/ - Configuration templates ✅ Clean root-level organization NEW PURPOSE: 🎯 Universal toolkit to deploy ANY Django project to VPS 🏷️ Auto-extracts GitHub repo names 🔄 Multi-project support with path-based routing 📡 GitHub webhook auto-deploy integration 🔒 Secure non-root deployment BENEFITS: - Deploy any Django project with one command - Zero configuration required - Professional deployment toolkit - Reusable for unlimited projects - Industry-standard VPS setup Now it's a PURE deployment toolkit, not a demo project! 🎉
This commit is contained in:
parent
c544d15e02
commit
996c50841b
186
README.md
186
README.md
@ -1,64 +1,164 @@
|
|||||||
# Django VPS Demo Project
|
# 🚀 Django VPS Deployment Toolkit
|
||||||
|
|
||||||
Simple Django project optimized for VPS deployment with multi-project support.
|
**Universal toolkit to deploy ANY Django project to VPS with zero configuration.**
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **Minimal Dependencies**: Clean, lightweight setup with only essential packages
|
- 🎯 **Universal** - Deploy any Django project from GitHub
|
||||||
- **Environment Configuration**: Production settings managed via environment variables
|
- 🏷️ **Auto-naming** - Extracts project name from repo URL
|
||||||
- **Database Flexible**: Works with SQLite for development and PostgreSQL for production
|
- 🔄 **Multi-project** - Unlimited Django projects per VPS
|
||||||
- **Static Files Handling**: Configured with WhiteNoise for efficient static file serving
|
- 🔧 **Zero config** - Automatic nginx + gunicorn + systemd setup
|
||||||
- **Contact Form**: Functional contact form with email validation
|
- 🚀 **One command** - Complete deployment in minutes
|
||||||
- **Simple Blog System**: Basic blog functionality with admin interface
|
- 🔒 **Secure** - Non-root deployment with proper permissions
|
||||||
- **Bootstrap UI**: Responsive design using Bootstrap 5
|
- 📡 **Auto-deploy** - GitHub webhook integration for CI/CD
|
||||||
- **Production Security**: Security headers, HTTPS support, and production optimizations
|
|
||||||
- **VPS Deployment**: Complete deployment scripts and configuration files
|
|
||||||
|
|
||||||
## 🛠 Tech Stack
|
## 🚀 Quick Deploy
|
||||||
|
|
||||||
- **Backend**: Django 4.2.7
|
### Setup VPS (Once per VPS)
|
||||||
- **Database**: SQLite (development) / PostgreSQL (production)
|
|
||||||
- **Web Server**: Gunicorn + Nginx
|
|
||||||
- **Frontend**: Bootstrap 5, Vanilla JavaScript
|
|
||||||
- **Static Files**: WhiteNoise
|
|
||||||
- **Configuration**: python-decouple
|
|
||||||
|
|
||||||
## 🚀 Deploy to VPS
|
|
||||||
|
|
||||||
### After VPS Reset:
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Fix SSH key
|
# 1. Fix SSH after VPS reset
|
||||||
ssh-keygen -f '/home/amit/.ssh/known_hosts' -R '69.62.81.168'
|
ssh-keygen -f '~/.ssh/known_hosts' -R 'YOUR_VPS_IP'
|
||||||
|
|
||||||
# 2. Setup django user
|
# 2. Upload and run user setup
|
||||||
scp setup-django-user.sh akvps:/root/
|
scp setup-django-user.sh root@YOUR_VPS_IP:/root/
|
||||||
ssh akvps "sudo bash /root/setup-django-user.sh"
|
ssh root@YOUR_VPS_IP "sudo bash /root/setup-django-user.sh"
|
||||||
|
|
||||||
# 3. Clone and deploy
|
# 3. Clone this toolkit
|
||||||
|
ssh root@YOUR_VPS_IP "cd /home/django && git clone https://github.com/thecyberlearn/hostinger-django-demo.git django-vps-toolkit"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deploy Your Project
|
### Deploy Any Django Project
|
||||||
```bash
|
```bash
|
||||||
ssh akvps "cd /home/django && git clone https://github.com/thecyberlearn/hostinger-django-demo.git"
|
# Deploy any Django project with one command!
|
||||||
ssh akvps "cd /home/django/hostinger-django-demo && sudo bash deploy/deploy-project.sh https://github.com/thecyberlearn/hostinger-django-demo.git"
|
ssh root@YOUR_VPS_IP "cd /home/django/django-vps-toolkit && sudo bash deploy-django-project.sh https://github.com/USER/PROJECT.git"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Your site**: http://69.62.81.168/
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# Deploy a blog
|
||||||
|
sudo bash deploy-django-project.sh https://github.com/johndoe/my-blog.git
|
||||||
|
# → Live at: http://YOUR_VPS_IP/my-blog/
|
||||||
|
|
||||||
## 📁 Key Files
|
# Deploy an e-commerce site
|
||||||
|
sudo bash deploy-django-project.sh https://github.com/company/shop-backend.git
|
||||||
|
# → Live at: http://YOUR_VPS_IP/shop-backend/
|
||||||
|
|
||||||
**Main Deployment Script:**
|
# Deploy a portfolio
|
||||||
- `deploy/deploy-project.sh` - Deploys any Django project with auto repo naming
|
sudo bash deploy-django-project.sh https://github.com/jane/portfolio-site.git
|
||||||
|
# → Live at: http://YOUR_VPS_IP/portfolio-site/
|
||||||
|
```
|
||||||
|
|
||||||
**Documentation:**
|
## 🎯 What It Does
|
||||||
- `MULTI_PROJECT_SETUP.md` - Complete multi-project guide
|
|
||||||
|
|
||||||
**Setup:**
|
1. **Extracts project name** from GitHub URL
|
||||||
- `setup-django-user.sh` - Create django user on fresh VPS
|
2. **Clones project** to `/var/www/PROJECT_NAME/`
|
||||||
- `requirements.txt` - Python dependencies
|
3. **Creates virtual environment** and installs dependencies
|
||||||
|
4. **Runs Django migrations** and collects static files
|
||||||
|
5. **Creates systemd service** `gunicorn-PROJECT_NAME.service`
|
||||||
|
6. **Configures nginx** for path-based routing
|
||||||
|
7. **Starts everything** and tests deployment
|
||||||
|
|
||||||
## 🎯 That's It!
|
## 📁 Toolkit Files
|
||||||
|
|
||||||
For detailed multi-project setup, see `MULTI_PROJECT_SETUP.md`
|
**🚀 Main Scripts:**
|
||||||
|
- `deploy-django-project.sh` - Deploy any Django project
|
||||||
|
- `setup-django-user.sh` - VPS user setup (run once)
|
||||||
|
|
||||||
Simple, clean, and no confusion! 🚀
|
**🔧 Advanced Features:**
|
||||||
|
- `setup-multi-webhook.sh` - GitHub auto-deploy webhooks
|
||||||
|
- `webhook-router.py` - Multi-project webhook handler
|
||||||
|
- `MULTI_PROJECT_SETUP.md` - Advanced webhook guide
|
||||||
|
|
||||||
|
**📝 Templates:**
|
||||||
|
- `templates/nginx.conf.template` - Nginx configuration
|
||||||
|
- `templates/gunicorn.service.template` - Systemd service
|
||||||
|
- `templates/production_settings.py` - Django production settings
|
||||||
|
|
||||||
|
## 🔄 Auto-Deploy Setup
|
||||||
|
|
||||||
|
Want GitHub auto-deploy like Render/Vercel?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup webhook system
|
||||||
|
sudo bash setup-multi-webhook.sh
|
||||||
|
|
||||||
|
# Add webhook to your GitHub repos:
|
||||||
|
# URL: http://YOUR_VPS_IP/webhook
|
||||||
|
# Secret: [from setup output]
|
||||||
|
# Events: Push events
|
||||||
|
|
||||||
|
# Now push to GitHub → Auto-deploy! 🚀
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 Examples
|
||||||
|
|
||||||
|
### Deploy Multiple Projects
|
||||||
|
```bash
|
||||||
|
# Each project gets its own URL path
|
||||||
|
sudo bash deploy-django-project.sh https://github.com/user/blog.git
|
||||||
|
sudo bash deploy-django-project.sh https://github.com/user/shop.git
|
||||||
|
sudo bash deploy-django-project.sh https://github.com/user/api.git
|
||||||
|
|
||||||
|
# Results:
|
||||||
|
# http://YOUR_VPS_IP/blog/
|
||||||
|
# http://YOUR_VPS_IP/shop/
|
||||||
|
# http://YOUR_VPS_IP/api/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Management
|
||||||
|
```bash
|
||||||
|
# Check project status
|
||||||
|
systemctl status gunicorn-blog.service
|
||||||
|
systemctl status gunicorn-shop.service
|
||||||
|
|
||||||
|
# View project logs
|
||||||
|
journalctl -u gunicorn-blog.service -f
|
||||||
|
|
||||||
|
# Restart project
|
||||||
|
systemctl restart gunicorn-blog.service
|
||||||
|
|
||||||
|
# Update project
|
||||||
|
cd /var/www/blog && sudo -u django git pull && systemctl restart gunicorn-blog.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Requirements
|
||||||
|
|
||||||
|
**Your Django Project Needs:**
|
||||||
|
- `requirements.txt` file
|
||||||
|
- Working `manage.py`
|
||||||
|
- Proper Django project structure
|
||||||
|
|
||||||
|
**VPS Requirements:**
|
||||||
|
- Ubuntu 20.04+ or similar
|
||||||
|
- Root/sudo access
|
||||||
|
- 1GB+ RAM recommended
|
||||||
|
|
||||||
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
|
**Deployment fails?**
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
journalctl -u gunicorn-PROJECT_NAME.service -f
|
||||||
|
|
||||||
|
# Test Django
|
||||||
|
cd /var/www/PROJECT_NAME
|
||||||
|
sudo -u django bash -c "source venv/bin/activate && python manage.py check"
|
||||||
|
|
||||||
|
# Test nginx
|
||||||
|
nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
**Can't access site?**
|
||||||
|
```bash
|
||||||
|
# Check services
|
||||||
|
systemctl status gunicorn-PROJECT_NAME.service nginx
|
||||||
|
|
||||||
|
# Check firewall
|
||||||
|
ufw status
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎉 Success!
|
||||||
|
|
||||||
|
You now have a **universal Django deployment toolkit** that can deploy any Django project to VPS with zero configuration!
|
||||||
|
|
||||||
|
**Just provide a GitHub URL and get a working Django site!** 🚀
|
||||||
@ -1,21 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from .models import Contact, BlogPost
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Contact)
|
|
||||||
class ContactAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ['name', 'email', 'subject', 'created_at']
|
|
||||||
list_filter = ['created_at']
|
|
||||||
search_fields = ['name', 'email', 'subject']
|
|
||||||
readonly_fields = ['created_at']
|
|
||||||
ordering = ['-created_at']
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(BlogPost)
|
|
||||||
class BlogPostAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ['title', 'is_published', 'created_at', 'updated_at']
|
|
||||||
list_filter = ['is_published', 'created_at']
|
|
||||||
search_fields = ['title', 'content']
|
|
||||||
prepopulated_fields = {'slug': ('title',)}
|
|
||||||
readonly_fields = ['created_at', 'updated_at']
|
|
||||||
ordering = ['-created_at']
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class CoreConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'core'
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
from .models import Contact
|
|
||||||
|
|
||||||
|
|
||||||
class ContactForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Contact
|
|
||||||
fields = ['name', 'email', 'subject', 'message']
|
|
||||||
widgets = {
|
|
||||||
'name': forms.TextInput(attrs={
|
|
||||||
'class': 'form-control',
|
|
||||||
'placeholder': 'Your Name'
|
|
||||||
}),
|
|
||||||
'email': forms.EmailInput(attrs={
|
|
||||||
'class': 'form-control',
|
|
||||||
'placeholder': 'your.email@example.com'
|
|
||||||
}),
|
|
||||||
'subject': forms.TextInput(attrs={
|
|
||||||
'class': 'form-control',
|
|
||||||
'placeholder': 'Subject'
|
|
||||||
}),
|
|
||||||
'message': forms.Textarea(attrs={
|
|
||||||
'class': 'form-control',
|
|
||||||
'rows': 5,
|
|
||||||
'placeholder': 'Your message...'
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
# Generated by Django 4.2.7 on 2025-08-29 16:12
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.utils.timezone
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='BlogPost',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('title', models.CharField(max_length=200)),
|
|
||||||
('slug', models.SlugField(unique=True)),
|
|
||||||
('content', models.TextField()),
|
|
||||||
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('is_published', models.BooleanField(default=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['-created_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Contact',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=100)),
|
|
||||||
('email', models.EmailField(max_length=254)),
|
|
||||||
('subject', models.CharField(max_length=200)),
|
|
||||||
('message', models.TextField()),
|
|
||||||
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['-created_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
|
|
||||||
class Contact(models.Model):
|
|
||||||
name = models.CharField(max_length=100)
|
|
||||||
email = models.EmailField()
|
|
||||||
subject = models.CharField(max_length=200)
|
|
||||||
message = models.TextField()
|
|
||||||
created_at = models.DateTimeField(default=timezone.now)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name} - {self.subject}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-created_at']
|
|
||||||
|
|
||||||
|
|
||||||
class BlogPost(models.Model):
|
|
||||||
title = models.CharField(max_length=200)
|
|
||||||
slug = models.SlugField(unique=True)
|
|
||||||
content = models.TextField()
|
|
||||||
created_at = models.DateTimeField(default=timezone.now)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
is_published = models.BooleanField(default=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-created_at']
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
.hero-section {
|
|
||||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Auto-hide alerts after 5 seconds
|
|
||||||
const alerts = document.querySelectorAll('.alert');
|
|
||||||
alerts.forEach(function(alert) {
|
|
||||||
setTimeout(function() {
|
|
||||||
const bsAlert = new bootstrap.Alert(alert);
|
|
||||||
bsAlert.close();
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Smooth scrolling for internal links
|
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
||||||
anchor.addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const target = document.querySelector(this.getAttribute('href'));
|
|
||||||
if (target) {
|
|
||||||
target.scrollIntoView({
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
{% extends 'core/base.html' %}
|
|
||||||
|
|
||||||
{% block title %}About - Django Demo Project{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container py-5">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8 mx-auto">
|
|
||||||
<h1 class="mb-4">About This Project</h1>
|
|
||||||
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Purpose</h5>
|
|
||||||
<p class="card-text">This Django demo project is designed as a production-ready template for testing VPS deployment workflows. It serves as a foundation for future Django projects with all the essential configurations already in place.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Features</h5>
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<li>✓ Minimal dependencies for fast deployment</li>
|
|
||||||
<li>✓ Environment-based configuration</li>
|
|
||||||
<li>✓ SQLite development / PostgreSQL production</li>
|
|
||||||
<li>✓ Static files handling with WhiteNoise</li>
|
|
||||||
<li>✓ Contact form functionality</li>
|
|
||||||
<li>✓ Simple blog system</li>
|
|
||||||
<li>✓ Bootstrap-based responsive UI</li>
|
|
||||||
<li>✓ Production security settings</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Tech Stack</h5>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<li><strong>Backend:</strong> Django 4.2.7</li>
|
|
||||||
<li><strong>Database:</strong> SQLite/PostgreSQL</li>
|
|
||||||
<li><strong>Server:</strong> Gunicorn</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<li><strong>Frontend:</strong> Bootstrap 5</li>
|
|
||||||
<li><strong>Static Files:</strong> WhiteNoise</li>
|
|
||||||
<li><strong>Config:</strong> python-decouple</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<a href="{% url 'contact' %}" class="btn btn-primary">Get in Touch</a>
|
|
||||||
<a href="{% url 'home' %}" class="btn btn-outline-primary">Back to Home</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{% block title %}Django Demo Project{% endblock %}</title>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
{% load static %}
|
|
||||||
<link rel="stylesheet" href="{% static 'core/css/style.css' %}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="{% url 'home' %}">Django Demo</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
|
||||||
<ul class="navbar-nav ms-auto">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'home' %}">Home</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'blog' %}">Blog</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'about' %}">About</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'contact' %}">Contact</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
{% if messages %}
|
|
||||||
<div class="container mt-3">
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
|
||||||
{{ message }}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% endblock %}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="bg-light text-center text-muted py-4 mt-5">
|
|
||||||
<div class="container">
|
|
||||||
<p>© 2024 Django Demo Project. Built for VPS deployment testing.</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
{% load static %}
|
|
||||||
<script src="{% static 'core/js/main.js' %}"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
{% extends 'core/base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Blog - Django Demo Project{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container py-5">
|
|
||||||
<h1 class="mb-4">Blog</h1>
|
|
||||||
|
|
||||||
{% if posts %}
|
|
||||||
<div class="row">
|
|
||||||
{% for post in posts %}
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">{{ post.title }}</h5>
|
|
||||||
<p class="card-text">{{ post.content|truncatewords:30 }}</p>
|
|
||||||
<small class="text-muted">Published on {{ post.created_at|date:"F d, Y" }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center py-5">
|
|
||||||
<h3 class="text-muted">No blog posts yet</h3>
|
|
||||||
<p>Blog posts will appear here once they're created in the admin panel.</p>
|
|
||||||
<a href="{% url 'home' %}" class="btn btn-primary">Back to Home</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
{% extends 'core/base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Contact - Django Demo Project{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container py-5">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<h1 class="mb-4">Contact Us</h1>
|
|
||||||
<p class="lead">Have questions about this Django demo project? Get in touch!</p>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.name.id_for_label }}" class="form-label">Name</label>
|
|
||||||
{{ form.name }}
|
|
||||||
{% if form.name.errors %}
|
|
||||||
<div class="text-danger">{{ form.name.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.email.id_for_label }}" class="form-label">Email</label>
|
|
||||||
{{ form.email }}
|
|
||||||
{% if form.email.errors %}
|
|
||||||
<div class="text-danger">{{ form.email.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.subject.id_for_label }}" class="form-label">Subject</label>
|
|
||||||
{{ form.subject }}
|
|
||||||
{% if form.subject.errors %}
|
|
||||||
<div class="text-danger">{{ form.subject.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.message.id_for_label }}" class="form-label">Message</label>
|
|
||||||
{{ form.message }}
|
|
||||||
{% if form.message.errors %}
|
|
||||||
<div class="text-danger">{{ form.message.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Send Message</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h5>Project Info</h5>
|
|
||||||
<p><strong>Django Version:</strong> 4.2.7</p>
|
|
||||||
<p><strong>Purpose:</strong> VPS Deployment Testing</p>
|
|
||||||
<p><strong>Features:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li>Contact Form</li>
|
|
||||||
<li>Blog System</li>
|
|
||||||
<li>Environment Configuration</li>
|
|
||||||
<li>Production Ready</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
{% extends 'core/base.html' %}
|
|
||||||
|
|
||||||
{% block title %}Home - Django Demo Project{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="hero-section bg-primary text-white py-5">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<h1 class="display-4">Django Demo Project</h1>
|
|
||||||
<p class="lead">A production-ready Django application template optimized for VPS hosting deployment.</p>
|
|
||||||
<a href="{% url 'contact' %}" class="btn btn-light btn-lg">Get Started</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container py-5">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<h2>Features</h2>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Production Ready</h5>
|
|
||||||
<p class="card-text">Configured with environment variables, security settings, and optimized for deployment.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Database Flexible</h5>
|
|
||||||
<p class="card-text">Works with SQLite for development and PostgreSQL for production.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">VPS Optimized</h5>
|
|
||||||
<p class="card-text">Includes deployment scripts and configuration for easy VPS hosting.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Minimal Dependencies</h5>
|
|
||||||
<p class="card-text">Clean, lightweight setup with only essential packages.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h3>Recent Blog Posts</h3>
|
|
||||||
{% if blog_posts %}
|
|
||||||
{% for post in blog_posts %}
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">{{ post.title }}</h5>
|
|
||||||
<p class="card-text">{{ post.content|truncatewords:20 }}</p>
|
|
||||||
<small class="text-muted">{{ post.created_at|date:"M d, Y" }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<a href="{% url 'blog' %}" class="btn btn-outline-primary">View All Posts</a>
|
|
||||||
{% else %}
|
|
||||||
<p class="text-muted">No blog posts yet. Check back later!</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('', views.home, name='home'),
|
|
||||||
path('contact/', views.contact, name='contact'),
|
|
||||||
path('blog/', views.blog, name='blog'),
|
|
||||||
path('about/', views.about, name='about'),
|
|
||||||
]
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
from django.shortcuts import render, redirect
|
|
||||||
from django.contrib import messages
|
|
||||||
from .models import BlogPost
|
|
||||||
from .forms import ContactForm
|
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
|
||||||
blog_posts = BlogPost.objects.filter(is_published=True)[:3]
|
|
||||||
return render(request, 'core/home.html', {'blog_posts': blog_posts})
|
|
||||||
|
|
||||||
|
|
||||||
def contact(request):
|
|
||||||
if request.method == 'POST':
|
|
||||||
form = ContactForm(request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
messages.success(request, 'Thank you for your message! We will get back to you soon.')
|
|
||||||
return redirect('contact')
|
|
||||||
else:
|
|
||||||
form = ContactForm()
|
|
||||||
|
|
||||||
return render(request, 'core/contact.html', {'form': form})
|
|
||||||
|
|
||||||
|
|
||||||
def blog(request):
|
|
||||||
posts = BlogPost.objects.filter(is_published=True)
|
|
||||||
return render(request, 'core/blog.html', {'posts': posts})
|
|
||||||
|
|
||||||
|
|
||||||
def about(request):
|
|
||||||
return render(request, 'core/about.html')
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
ASGI config for demo_project project.
|
|
||||||
|
|
||||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo_project.settings')
|
|
||||||
|
|
||||||
application = get_asgi_application()
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
"""
|
|
||||||
Django settings for demo_project project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 4.2.7.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.2/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from decouple import config
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = config('SECRET_KEY', default='django-insecure-%%73_#43bfj4u2^usxt&(hmx$*r!&@w6z7!r-psjm%gu_5(s35')
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = config('DEBUG', default=True, cast=bool)
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')])
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
'core',
|
|
||||||
]
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'demo_project.urls'
|
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'DIRS': [],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.request',
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'demo_project.wsgi.application'
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
|
||||||
|
|
||||||
# Database configuration
|
|
||||||
if config('DATABASE_URL', default=None):
|
|
||||||
import dj_database_url
|
|
||||||
DATABASES = {
|
|
||||||
'default': dj_database_url.parse(config('DATABASE_URL'))
|
|
||||||
}
|
|
||||||
elif config('USE_POSTGRES', default=False, cast=bool):
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
|
||||||
'NAME': config('DB_NAME', default='demo_db'),
|
|
||||||
'USER': config('DB_USER', default='demo_user'),
|
|
||||||
'PASSWORD': config('DB_PASSWORD', default=''),
|
|
||||||
'HOST': config('DB_HOST', default='localhost'),
|
|
||||||
'PORT': config('DB_PORT', default='5432'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
|
||||||
STATICFILES_DIRS = []
|
|
||||||
|
|
||||||
# Whitenoise settings
|
|
||||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
|
||||||
|
|
||||||
# Default primary key field type
|
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
||||||
|
|
||||||
# Production security settings
|
|
||||||
if not DEBUG:
|
|
||||||
SECURE_BROWSER_XSS_FILTER = True
|
|
||||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
||||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
||||||
SECURE_HSTS_SECONDS = 31536000
|
|
||||||
SECURE_REDIRECT_EXEMPT = []
|
|
||||||
SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=False, cast=bool)
|
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
|
||||||
SESSION_COOKIE_SECURE = True
|
|
||||||
CSRF_COOKIE_SECURE = True
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
"""
|
|
||||||
URL configuration for demo_project project.
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.urls import include, path
|
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import path, include
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('admin/', admin.site.urls),
|
|
||||||
path('', include('core.urls')),
|
|
||||||
]
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
WSGI config for demo_project project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo_project.settings')
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
|
||||||
@ -163,8 +163,8 @@ DJANGO_PROJECT_DIR=$(sudo -u django find "$PROJECT_PATH" -name "settings.py" -ex
|
|||||||
DJANGO_PROJECT_NAME=$(basename "$DJANGO_PROJECT_DIR")
|
DJANGO_PROJECT_NAME=$(basename "$DJANGO_PROJECT_DIR")
|
||||||
|
|
||||||
# Create Gunicorn configuration
|
# Create Gunicorn configuration
|
||||||
mkdir -p "$PROJECT_PATH/deploy"
|
mkdir -p "$PROJECT_PATH/config"
|
||||||
cat > "$PROJECT_PATH/deploy/gunicorn.conf.py" << EOF
|
cat > "$PROJECT_PATH/config/gunicorn.conf.py" << EOF
|
||||||
# Gunicorn configuration for $PROJECT_NAME
|
# Gunicorn configuration for $PROJECT_NAME
|
||||||
bind = "unix:/run/gunicorn-$PROJECT_NAME.sock"
|
bind = "unix:/run/gunicorn-$PROJECT_NAME.sock"
|
||||||
workers = 3
|
workers = 3
|
||||||
@ -204,7 +204,7 @@ WorkingDirectory=$PROJECT_PATH
|
|||||||
Environment=PYTHONPATH=$PROJECT_PATH
|
Environment=PYTHONPATH=$PROJECT_PATH
|
||||||
EnvironmentFile=$PROJECT_PATH/.env
|
EnvironmentFile=$PROJECT_PATH/.env
|
||||||
ExecStart=$PROJECT_PATH/venv/bin/gunicorn \\
|
ExecStart=$PROJECT_PATH/venv/bin/gunicorn \\
|
||||||
--config $PROJECT_PATH/deploy/gunicorn.conf.py \\
|
--config $PROJECT_PATH/config/gunicorn.conf.py \\
|
||||||
$DJANGO_PROJECT_NAME.wsgi:application
|
$DJANGO_PROJECT_NAME.wsgi:application
|
||||||
ExecReload=/bin/kill -s HUP \$MAINPID
|
ExecReload=/bin/kill -s HUP \$MAINPID
|
||||||
KillMode=mixed
|
KillMode=mixed
|
||||||
@ -1,35 +0,0 @@
|
|||||||
"""Gunicorn configuration for production deployment"""
|
|
||||||
|
|
||||||
import multiprocessing
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Server socket
|
|
||||||
bind = "127.0.0.1:8000"
|
|
||||||
backlog = 2048
|
|
||||||
|
|
||||||
# Worker processes
|
|
||||||
workers = multiprocessing.cpu_count() * 2 + 1
|
|
||||||
worker_class = "sync"
|
|
||||||
worker_connections = 1000
|
|
||||||
timeout = 30
|
|
||||||
keepalive = 2
|
|
||||||
max_requests = 1000
|
|
||||||
max_requests_jitter = 100
|
|
||||||
|
|
||||||
# Restart workers after this many requests, with up to max_requests_jitter additional requests
|
|
||||||
preload_app = True
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
accesslog = "/var/log/django/access.log"
|
|
||||||
errorlog = "/var/log/django/error.log"
|
|
||||||
loglevel = "info"
|
|
||||||
|
|
||||||
# Process naming
|
|
||||||
proc_name = "django_demo_project"
|
|
||||||
|
|
||||||
# Server mechanics
|
|
||||||
daemon = False
|
|
||||||
pidfile = "/var/run/gunicorn/django_demo.pid"
|
|
||||||
user = "www-data"
|
|
||||||
group = "www-data"
|
|
||||||
tmp_upload_dir = None
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name yourdomain.com www.yourdomain.com;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
|
||||||
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
|
||||||
|
|
||||||
# Gzip compression
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_min_length 1024;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_types
|
|
||||||
text/plain
|
|
||||||
text/css
|
|
||||||
text/xml
|
|
||||||
text/javascript
|
|
||||||
application/json
|
|
||||||
application/javascript
|
|
||||||
application/xml+rss
|
|
||||||
application/atom+xml
|
|
||||||
image/svg+xml;
|
|
||||||
|
|
||||||
# Static files
|
|
||||||
location /static/ {
|
|
||||||
alias /home/ubuntu/django-demo/staticfiles/;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Media files (if you add file uploads later)
|
|
||||||
location /media/ {
|
|
||||||
alias /home/ubuntu/django-demo/media/;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Django application
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1: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;
|
|
||||||
proxy_redirect off;
|
|
||||||
|
|
||||||
# Timeouts
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Block access to sensitive files
|
|
||||||
location ~* /\.(?!well-known\/) {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* /(requirements\.txt|\.env|deploy/) {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Django Demo Project Gunicorn daemon
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=notify
|
|
||||||
User=www-data
|
|
||||||
Group=www-data
|
|
||||||
RuntimeDirectory=gunicorn
|
|
||||||
WorkingDirectory=/home/ubuntu/django-demo
|
|
||||||
ExecStart=/home/ubuntu/django-demo/venv/bin/gunicorn --config /home/ubuntu/django-demo/deploy/gunicorn.conf.py demo_project.wsgi:application
|
|
||||||
ExecReload=/bin/kill -s HUP $MAINPID
|
|
||||||
KillMode=mixed
|
|
||||||
TimeoutStopSec=5
|
|
||||||
PrivateTmp=true
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
Environment=DJANGO_SETTINGS_MODULE=demo_project.settings
|
|
||||||
EnvironmentFile=/home/ubuntu/django-demo/.env
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@ -1,248 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
GitHub Webhook Receiver for Auto-Deployment
|
|
||||||
Listens for GitHub push events and triggers automatic deployment
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
import subprocess
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
from flask import Flask, request, jsonify
|
|
||||||
from threading import Thread
|
|
||||||
import time
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', 'your-webhook-secret-here')
|
|
||||||
REPO_PATH = '/var/www/django-app'
|
|
||||||
ALLOWED_BRANCHES = ['main', 'master']
|
|
||||||
LOG_FILE = '/var/log/django/webhook.log'
|
|
||||||
|
|
||||||
# Setup logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[
|
|
||||||
logging.FileHandler(LOG_FILE),
|
|
||||||
logging.StreamHandler()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
def verify_signature(payload_body, signature_header):
|
|
||||||
"""Verify GitHub webhook signature"""
|
|
||||||
if not signature_header:
|
|
||||||
return False
|
|
||||||
|
|
||||||
hash_object = hmac.new(
|
|
||||||
WEBHOOK_SECRET.encode('utf-8'),
|
|
||||||
payload_body,
|
|
||||||
hashlib.sha256
|
|
||||||
)
|
|
||||||
expected_signature = "sha256=" + hash_object.hexdigest()
|
|
||||||
|
|
||||||
return hmac.compare_digest(expected_signature, signature_header)
|
|
||||||
|
|
||||||
def run_deployment():
|
|
||||||
"""Execute deployment in background thread"""
|
|
||||||
try:
|
|
||||||
logger.info("🚀 Starting deployment...")
|
|
||||||
|
|
||||||
# Change to app directory
|
|
||||||
os.chdir(REPO_PATH)
|
|
||||||
|
|
||||||
# Run deployment script
|
|
||||||
result = subprocess.run([
|
|
||||||
'sudo', '-u', 'django', 'bash', '-c',
|
|
||||||
f'''
|
|
||||||
cd {REPO_PATH}
|
|
||||||
|
|
||||||
# Store current commit for rollback
|
|
||||||
echo "$(git rev-parse HEAD)" > /tmp/last_working_commit.txt
|
|
||||||
|
|
||||||
# Pull latest changes
|
|
||||||
git fetch origin
|
|
||||||
git reset --hard origin/main
|
|
||||||
|
|
||||||
# Activate virtual environment and update
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# Install/update dependencies
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Run Django management commands
|
|
||||||
python manage.py migrate
|
|
||||||
python manage.py collectstatic --noinput
|
|
||||||
|
|
||||||
# Test if Django can start (quick check)
|
|
||||||
python manage.py check --deploy
|
|
||||||
'''
|
|
||||||
], capture_output=True, text=True, timeout=300)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
# Restart services
|
|
||||||
subprocess.run(['systemctl', 'restart', 'gunicorn.service'], check=True)
|
|
||||||
|
|
||||||
# Wait a moment and check if service is running
|
|
||||||
time.sleep(3)
|
|
||||||
service_check = subprocess.run(['systemctl', 'is-active', 'gunicorn.service'],
|
|
||||||
capture_output=True, text=True)
|
|
||||||
|
|
||||||
if service_check.stdout.strip() == 'active':
|
|
||||||
logger.info("✅ Deployment successful!")
|
|
||||||
|
|
||||||
# Send success notification (optional)
|
|
||||||
send_notification("✅ Deployment successful!", "success")
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.error("❌ Service failed to start after deployment")
|
|
||||||
rollback()
|
|
||||||
else:
|
|
||||||
logger.error(f"❌ Deployment failed: {result.stderr}")
|
|
||||||
rollback()
|
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
logger.error("❌ Deployment timed out")
|
|
||||||
rollback()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Deployment error: {str(e)}")
|
|
||||||
rollback()
|
|
||||||
|
|
||||||
def rollback():
|
|
||||||
"""Rollback to previous working commit"""
|
|
||||||
try:
|
|
||||||
logger.info("🔄 Rolling back to previous commit...")
|
|
||||||
|
|
||||||
if os.path.exists('/tmp/last_working_commit.txt'):
|
|
||||||
with open('/tmp/last_working_commit.txt', 'r') as f:
|
|
||||||
last_commit = f.read().strip()
|
|
||||||
|
|
||||||
subprocess.run([
|
|
||||||
'sudo', '-u', 'django', 'bash', '-c',
|
|
||||||
f'cd {REPO_PATH} && git reset --hard {last_commit}'
|
|
||||||
], check=True)
|
|
||||||
|
|
||||||
subprocess.run(['systemctl', 'restart', 'gunicorn.service'], check=True)
|
|
||||||
logger.info("✅ Rollback completed")
|
|
||||||
send_notification("🔄 Rolled back due to deployment failure", "warning")
|
|
||||||
else:
|
|
||||||
logger.error("❌ No previous commit found for rollback")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Rollback failed: {str(e)}")
|
|
||||||
|
|
||||||
def send_notification(message, status="info"):
|
|
||||||
"""Send deployment notification (extend this for Slack/Discord/Email)"""
|
|
||||||
logger.info(f"📢 Notification: {message}")
|
|
||||||
|
|
||||||
# You can extend this to send notifications to:
|
|
||||||
# - Slack webhook
|
|
||||||
# - Discord webhook
|
|
||||||
# - Email
|
|
||||||
# - SMS
|
|
||||||
|
|
||||||
# Example Slack notification (uncomment and configure):
|
|
||||||
# import requests
|
|
||||||
# slack_webhook = "YOUR_SLACK_WEBHOOK_URL"
|
|
||||||
# requests.post(slack_webhook, json={"text": f"🚀 Django App: {message}"})
|
|
||||||
|
|
||||||
@app.route('/webhook', methods=['POST'])
|
|
||||||
def handle_webhook():
|
|
||||||
"""Handle GitHub webhook"""
|
|
||||||
|
|
||||||
# Verify signature
|
|
||||||
signature = request.headers.get('X-Hub-Signature-256')
|
|
||||||
if not verify_signature(request.data, signature):
|
|
||||||
logger.warning("❌ Invalid webhook signature")
|
|
||||||
return jsonify({"error": "Invalid signature"}), 403
|
|
||||||
|
|
||||||
# Parse payload
|
|
||||||
try:
|
|
||||||
payload = request.json
|
|
||||||
except:
|
|
||||||
logger.warning("❌ Invalid JSON payload")
|
|
||||||
return jsonify({"error": "Invalid JSON"}), 400
|
|
||||||
|
|
||||||
# Check if it's a push event
|
|
||||||
if request.headers.get('X-GitHub-Event') != 'push':
|
|
||||||
logger.info(f"ℹ️ Ignoring non-push event: {request.headers.get('X-GitHub-Event')}")
|
|
||||||
return jsonify({"message": "Not a push event"}), 200
|
|
||||||
|
|
||||||
# Extract branch name
|
|
||||||
ref = payload.get('ref', '')
|
|
||||||
branch = ref.replace('refs/heads/', '')
|
|
||||||
|
|
||||||
# Check if it's a branch we care about
|
|
||||||
if branch not in ALLOWED_BRANCHES:
|
|
||||||
logger.info(f"ℹ️ Ignoring push to branch: {branch}")
|
|
||||||
return jsonify({"message": f"Ignoring branch {branch}"}), 200
|
|
||||||
|
|
||||||
# Log the deployment request
|
|
||||||
commit_hash = payload.get('after', 'unknown')
|
|
||||||
commit_message = ""
|
|
||||||
if payload.get('head_commit'):
|
|
||||||
commit_message = payload['head_commit'].get('message', '')
|
|
||||||
|
|
||||||
logger.info(f"🔔 Deployment triggered by push to {branch}")
|
|
||||||
logger.info(f"📝 Commit: {commit_hash[:8]} - {commit_message[:100]}")
|
|
||||||
|
|
||||||
# Start deployment in background thread
|
|
||||||
deployment_thread = Thread(target=run_deployment)
|
|
||||||
deployment_thread.start()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"message": "Deployment started",
|
|
||||||
"branch": branch,
|
|
||||||
"commit": commit_hash[:8]
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
|
||||||
def health_check():
|
|
||||||
"""Health check endpoint"""
|
|
||||||
return jsonify({
|
|
||||||
"status": "healthy",
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
"repo_path": REPO_PATH
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route('/status', methods=['GET'])
|
|
||||||
def deployment_status():
|
|
||||||
"""Get current deployment status"""
|
|
||||||
try:
|
|
||||||
# Check if services are running
|
|
||||||
gunicorn_status = subprocess.run(['systemctl', 'is-active', 'gunicorn.service'],
|
|
||||||
capture_output=True, text=True)
|
|
||||||
nginx_status = subprocess.run(['systemctl', 'is-active', 'nginx'],
|
|
||||||
capture_output=True, text=True)
|
|
||||||
|
|
||||||
# Get current commit
|
|
||||||
os.chdir(REPO_PATH)
|
|
||||||
commit_result = subprocess.run(['git', 'rev-parse', 'HEAD'],
|
|
||||||
capture_output=True, text=True)
|
|
||||||
current_commit = commit_result.stdout.strip()[:8] if commit_result.returncode == 0 else "unknown"
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"gunicorn": gunicorn_status.stdout.strip(),
|
|
||||||
"nginx": nginx_status.stdout.strip(),
|
|
||||||
"current_commit": current_commit,
|
|
||||||
"timestamp": datetime.now().isoformat()
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Create log directory if it doesn't exist
|
|
||||||
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
|
||||||
|
|
||||||
logger.info("🚀 Starting GitHub webhook receiver...")
|
|
||||||
logger.info(f"📁 Monitoring repository: {REPO_PATH}")
|
|
||||||
logger.info(f"🌿 Allowed branches: {ALLOWED_BRANCHES}")
|
|
||||||
|
|
||||||
# Run Flask app
|
|
||||||
app.run(host='127.0.0.1', port=8001, debug=False)
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
"""
|
|
||||||
Auto-Deployment Service Setup Script
|
|
||||||
Sets up the webhook receiver as a systemd service
|
|
||||||
"""
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🔄 Setting up Auto-Deployment Service..."
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
WEBHOOK_SECRET=${1:-$(openssl rand -hex 32)}
|
|
||||||
SERVICE_USER="django"
|
|
||||||
APP_PATH="/var/www/django-app"
|
|
||||||
WEBHOOK_PATH="$APP_PATH/deploy/webhook-receiver.py"
|
|
||||||
|
|
||||||
# Colors
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
# Install Flask if not present
|
|
||||||
echo -e "${YELLOW}📦 Installing Flask...${NC}"
|
|
||||||
sudo -u django bash -c "cd $APP_PATH && source venv/bin/activate && pip install flask"
|
|
||||||
|
|
||||||
# Create webhook secret file
|
|
||||||
echo -e "${YELLOW}🔐 Setting up webhook secret...${NC}"
|
|
||||||
echo "WEBHOOK_SECRET=$WEBHOOK_SECRET" > /var/www/django-app/.env.webhook
|
|
||||||
chown django:www-data /var/www/django-app/.env.webhook
|
|
||||||
chmod 600 /var/www/django-app/.env.webhook
|
|
||||||
|
|
||||||
echo -e "${GREEN}🔑 Webhook Secret: $WEBHOOK_SECRET${NC}"
|
|
||||||
echo -e "${YELLOW}📝 Save this secret - you'll need it for GitHub webhook configuration!${NC}"
|
|
||||||
|
|
||||||
# Create systemd service file
|
|
||||||
echo -e "${YELLOW}⚙️ Creating systemd service...${NC}"
|
|
||||||
cat > /etc/systemd/system/django-webhook.service << EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Django Auto-Deployment Webhook Receiver
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=$SERVICE_USER
|
|
||||||
Group=www-data
|
|
||||||
WorkingDirectory=$APP_PATH
|
|
||||||
Environment=PYTHONPATH=$APP_PATH
|
|
||||||
EnvironmentFile=$APP_PATH/.env.webhook
|
|
||||||
ExecStart=$APP_PATH/venv/bin/python $WEBHOOK_PATH
|
|
||||||
Restart=always
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Create nginx configuration for webhook
|
|
||||||
echo -e "${YELLOW}🌐 Configuring Nginx proxy...${NC}"
|
|
||||||
cat > /etc/nginx/sites-available/django-webhook << 'EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name webhook.YOUR_DOMAIN.com; # Replace with your subdomain
|
|
||||||
|
|
||||||
location /webhook {
|
|
||||||
proxy_pass http://127.0.0.1:8001/webhook;
|
|
||||||
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 /health {
|
|
||||||
proxy_pass http://127.0.0.1:8001/health;
|
|
||||||
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 /status {
|
|
||||||
proxy_pass http://127.0.0.1:8001/status;
|
|
||||||
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;
|
|
||||||
|
|
||||||
# Optional: Add basic auth for status endpoint
|
|
||||||
# auth_basic "Deployment Status";
|
|
||||||
# auth_basic_user_file /etc/nginx/.htpasswd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Enable webhook nginx site (optional - you might want to use main domain with /webhook path)
|
|
||||||
echo -e "${YELLOW}ℹ️ Webhook Nginx config created at /etc/nginx/sites-available/django-webhook${NC}"
|
|
||||||
echo -e "${YELLOW}ℹ️ You can enable it with: ln -s /etc/nginx/sites-available/django-webhook /etc/nginx/sites-enabled/${NC}"
|
|
||||||
|
|
||||||
# Or add webhook endpoint to existing site
|
|
||||||
echo -e "${YELLOW}🔧 Adding webhook endpoint to main site...${NC}"
|
|
||||||
MAIN_NGINX_CONFIG="/etc/nginx/sites-available/django-app"
|
|
||||||
if [ -f "$MAIN_NGINX_CONFIG" ]; then
|
|
||||||
# Add webhook location block before the last closing brace
|
|
||||||
sed -i '/^}/i\
|
|
||||||
# GitHub Webhook endpoint\
|
|
||||||
location /webhook {\
|
|
||||||
proxy_pass http://127.0.0.1:8001/webhook;\
|
|
||||||
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;\
|
|
||||||
}\
|
|
||||||
\
|
|
||||||
# Deployment status endpoint\
|
|
||||||
location /deploy-status {\
|
|
||||||
proxy_pass http://127.0.0.1:8001/status;\
|
|
||||||
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;\
|
|
||||||
}' "$MAIN_NGINX_CONFIG"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Make webhook receiver executable
|
|
||||||
chmod +x $WEBHOOK_PATH
|
|
||||||
|
|
||||||
# Start and enable services
|
|
||||||
echo -e "${YELLOW}🚀 Starting services...${NC}"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable django-webhook.service
|
|
||||||
systemctl start django-webhook.service
|
|
||||||
|
|
||||||
# Restart nginx
|
|
||||||
nginx -t && systemctl restart nginx
|
|
||||||
|
|
||||||
# Check service status
|
|
||||||
if systemctl is-active --quiet django-webhook.service; then
|
|
||||||
echo -e "${GREEN}✅ Webhook service is running${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ Webhook service failed to start${NC}"
|
|
||||||
echo "Check logs: journalctl -u django-webhook.service -f"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}🎉 Auto-Deployment Setup Complete!${NC}"
|
|
||||||
echo
|
|
||||||
echo -e "${YELLOW}📋 Next Steps:${NC}"
|
|
||||||
echo -e "1. 🔐 Webhook Secret: ${GREEN}$WEBHOOK_SECRET${NC}"
|
|
||||||
echo -e "2. 🌐 Webhook URL: ${GREEN}http://YOUR_VPS_IP/webhook${NC}"
|
|
||||||
echo -e "3. 📝 Go to GitHub → Settings → Webhooks → Add webhook"
|
|
||||||
echo -e "4. 🔧 Configure webhook:"
|
|
||||||
echo -e " - Payload URL: http://YOUR_VPS_IP/webhook"
|
|
||||||
echo -e " - Content type: application/json"
|
|
||||||
echo -e " - Secret: $WEBHOOK_SECRET"
|
|
||||||
echo -e " - Events: Just the push event"
|
|
||||||
echo
|
|
||||||
echo -e "${YELLOW}🔍 Monitoring:${NC}"
|
|
||||||
echo -e "- Service logs: ${GREEN}journalctl -u django-webhook.service -f${NC}"
|
|
||||||
echo -e "- Deployment status: ${GREEN}http://YOUR_VPS_IP/deploy-status${NC}"
|
|
||||||
echo -e "- Health check: ${GREEN}http://YOUR_VPS_IP/health${NC}"
|
|
||||||
echo
|
|
||||||
echo -e "${GREEN}🚀 Your VPS now works like Render - just push to GitHub!${NC}"
|
|
||||||
22
manage.py
22
manage.py
@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""Django's command-line utility for administrative tasks."""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run administrative tasks."""
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo_project.settings')
|
|
||||||
try:
|
|
||||||
from django.core.management import execute_from_command_line
|
|
||||||
except ImportError as exc:
|
|
||||||
raise ImportError(
|
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
|
||||||
"forget to activate a virtual environment?"
|
|
||||||
) from exc
|
|
||||||
execute_from_command_line(sys.argv)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
# Core Django
|
|
||||||
Django==4.2.7
|
|
||||||
|
|
||||||
# Configuration & Environment
|
|
||||||
python-decouple==3.8
|
|
||||||
dj-database-url==2.1.0
|
|
||||||
|
|
||||||
# Production Server & Static Files
|
|
||||||
gunicorn==21.2.0
|
|
||||||
whitenoise==6.6.0
|
|
||||||
|
|
||||||
# Database (PostgreSQL)
|
|
||||||
psycopg2-binary==2.9.7
|
|
||||||
Loading…
Reference in New Issue
Block a user