Skip to content

Commit 3e1d8dc

Browse files
authored
Merge pull request #8 from omidcodes/008-fix-deployment-and-docker-compose
Add Nginx reverse proxy & fix docker-compose
2 parents 90dddf4 + 753571d commit 3e1d8dc

11 files changed

Lines changed: 155 additions & 78 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ WORKDIR /app
1010

1111
# Install dependencies
1212
COPY requirements.txt .
13-
RUN pip install --upgrade pip && pip install -r requirements.txt
13+
RUN pip install --upgrade pip && pip install -r requirements.txt && apt-get install -y netcat
1414

1515
# Copy project files
1616
COPY . .

README.md

Lines changed: 67 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,40 @@
1-
21
# 🧩 TaskFlow API
32

4-
A Django RESTful API for managing personal or team tasks — featuring PostgreSQL, RabbitMQ, Docker support, and developer/production-ready configurations.
3+
A Django RESTful API for managing personal or team tasks — featuring PostgreSQL, RabbitMQ, Celery, and Nginx in a Dockerized production setup.
54

65
---
76

87
## 🚀 Features
98

109
- ✅ Django 5 + Django REST Framework
11-
- ✅ PostgreSQL database (via Docker)
12-
- ✅ RabbitMQ for background tasks (Celery integrated)
13-
- ✅ Asynchronous task logging using Celery
10+
- ✅ PostgreSQL database (Dockerized)
11+
- ✅ RabbitMQ for background tasks (Celery-integrated)
12+
- ✅ Celery task queue for async logging
13+
- ✅ Gunicorn for WSGI-based production serving
14+
- ✅ Nginx reverse proxy for HTTP routing and static file delivery
1415
- ✅ Environment config with `.env` and `python-decouple`
1516
- ✅ Swagger UI for API documentation
16-
- ✅ Containerized with Docker
17-
- ✅ CLI scripts for development and production modes
18-
- ✅ Pytest-based testing with coverage
17+
- ✅ Docker & Docker Compose for development and deployment
18+
- ✅ Pytest-based testing with coverage reporting
1919

2020
---
2121

2222
## 📁 Project Structure
2323

2424
```
2525
taskflow-api/
26-
├── taskflow_api/ # Django project (includes celery.py)
27-
├── tasks/ # App: task models, views, serializers, signals, celery tasks
28-
├── tests/ # Pytest tests for models, API, celery tasks
26+
├── taskflow_api/ # Django project (with celery.py)
27+
├── tasks/ # App: models, views, serializers, signals, celery tasks
28+
├── tests/ # Pytest tests for models, views, celery
29+
├── Dockerfile # Docker image for Django (Gunicorn inside)
30+
├── docker-compose.yml # Full stack (Django, DB, Celery, Nginx, RabbitMQ)
31+
├── nginx.conf # Nginx config for reverse proxy
32+
├── .env # Environment variables
33+
├── logs/ # Log folder (created if missing)
2934
├── requirements.txt # Python dependencies
30-
├── Dockerfile # Production image for gunicorn
31-
├── docker-compose.yml # DB and RabbitMQ container setup
32-
├── .env # Environment configuration
33-
├── logs/ # Directory for activity logs (auto-created)
34-
├── run_server.sh # Run production server (Gunicorn)
35-
├── start-dev-services.sh # Run DB + RabbitMQ for development
36-
├── lint-clean.sh # Ruff lint & formatting script
35+
├── run_server.sh # Start all services in production mode
36+
├── start-dev-services.sh # Run only DB & RabbitMQ for local development
37+
├── lint-clean.sh # Format & lint Python code using Ruff
3738
└── pytest.ini # Pytest configuration
3839
```
3940

@@ -43,21 +44,21 @@ taskflow-api/
4344

4445
- Python 3.12+
4546
- Docker & Docker Compose
46-
- Virtualenv (optional but recommended)
47+
- (Optional) Virtualenv for local development
4748

4849
---
4950

5051
## 📦 Setup Instructions
5152

52-
### 🔧 1. Install Python Dependencies
53+
### 🔧 1. Create Virtual Environment (Optional)
5354
```bash
5455
python3 -m venv env
5556
source env/bin/activate
5657
pip install -r requirements.txt
5758
```
5859

5960
### 🔧 2. Configure Environment
60-
Edit `.env` (already provided):
61+
Edit the `.env` file:
6162
```dotenv
6263
DEBUG=True
6364
SECRET_KEY=your-secret-key
@@ -74,101 +75,100 @@ CELERY_BROKER_URL=amqp://guest:guest@localhost:5672//
7475

7576
---
7677

77-
## 🧪 Development Mode
78+
## 🧪 Development Mode (Local Python)
7879

79-
Use this when you want to run Django locally (`runserver`) and containers only for DB/RabbitMQ.
80+
Run Django and Celery locally. Use Docker for DB & RabbitMQ only.
8081

81-
### ▶️ Start Docker Services:
8282
```bash
83-
./start-dev-services.sh
83+
./start-dev-services.sh # Start db + rabbitmq only
84+
python manage.py runserver # Run Django locally
85+
celery -A taskflow_api worker --loglevel=info # Start Celery
8486
```
8587

86-
> This will:
87-
> - Start PostgreSQL and RabbitMQ containers
88-
> - Stop and remove any running web container
89-
> - Run DB migrations automatically
90-
91-
### ▶️ Then Run Django:
92-
```bash
93-
python3 manage.py runserver
94-
```
95-
96-
### ▶️ Run Celery Worker:
97-
```bash
98-
celery -A taskflow_api worker --loglevel=info
99-
```
100-
101-
Open:
102-
- Swagger docs: http://localhost:8000/docs/
103-
- API root: http://localhost:8000/api/tasks/
88+
> Local URLs:
89+
> - API: http://localhost:8000/api/tasks/
90+
> - Docs: http://localhost:8000/docs/
10491
10592
---
10693

107-
## 🧪 Run Tests and Coverage
94+
## 🧪 Run Tests
10895

10996
### ▶️ Run all tests
11097
```bash
11198
pytest
11299
```
113100

114-
### ▶️ Run tests **with coverage** (after installing `pytest-cov`)
101+
### ▶️ With coverage
115102
```bash
116103
pytest --cov=. --cov-report=term-missing
117104
```
118105

119-
### ▶️ (Optional) Generate HTML coverage report
106+
### ▶️ (Optional) HTML Coverage Report
120107
```bash
121108
pytest --cov=. --cov-report=html
122109
# Open htmlcov/index.html in your browser
123110
```
124111

125112
---
126113

127-
## 🧩 Celery Logging Task
128-
129-
When a task is created through the API, a Celery worker will automatically:
114+
## 🧩 Celery Background Logging
130115

131-
- Run `log_task_action` in the background using `celery -A taskflow_api worker --loglevel=info`
132-
- Write an entry like this to `logs/task_activity.log`:
116+
When a task is created via API, a background task (`log_task_action`) is triggered:
133117

118+
- Logs to `logs/task_activity.log`
119+
- Format:
134120
```
135121
[2025-09-14 19:45:00] Task #12 ('Example Task') was created via Celery background task.
136122
```
137123

138124
---
139125

140-
## 🏭 Production Mode (Dockerized Web)
126+
## 🏭 Production Mode (Dockerized Full Stack)
141127

142-
### ▶️ Build & Run All Services:
128+
### ▶️ Start All Services
143129
```bash
144130
./run_server.sh
145131
```
146132

147-
> This uses Docker to run:
148-
> - Django (with Gunicorn)
149-
> - PostgreSQL
150-
> - RabbitMQ
133+
This command will:
134+
- Build the Docker image
135+
- Run Django with Gunicorn
136+
- Serve via Nginx on port `80`
137+
- Collect static files into a volume
138+
- Expose the full app on http://localhost/
151139

152-
You can also run manually:
140+
> Alternatively:
153141
```bash
154142
docker compose up --build
155143
```
156144

157145
---
158146

147+
## 🌐 Accessing App
148+
149+
- Web App: [http://localhost/](http://localhost/)
150+
- API: [http://localhost/api/tasks/](http://localhost/api/tasks/)
151+
- Swagger Docs: [http://localhost/docs/](http://localhost/docs/)
152+
- RabbitMQ UI: [http://localhost:15672](http://localhost:15672) (user/pass: guest/guest)
153+
154+
---
155+
159156
## 🗃️ Tech Stack
160157

161-
- **Backend**: Django 5, DRF
162-
- **Database**: PostgreSQL (Docker)
163-
- **Broker**: RabbitMQ (Docker)
164-
- **Background Jobs**: Celery (activity logging)
165-
- **Containerization**: Docker, Docker Compose
166-
- **Testing**: Pytest, pytest-django, pytest-cov
167-
- **Linting**: Ruff
168-
- **CI-ready**: Gunicorn + environment-based config
158+
| Layer | Tech |
159+
|---------------|-------------------------|
160+
| Backend | Django 5 + DRF |
161+
| Database | PostgreSQL |
162+
| Broker | RabbitMQ |
163+
| Async Tasks | Celery |
164+
| Server | Gunicorn + Nginx |
165+
| Containers | Docker Compose |
166+
| Testing | Pytest + pytest-cov |
167+
| Linting | Ruff |
168+
| Deployment | Shell scripts + volumes |
169169

170170
---
171171

172172
## 📜 License
173173

174-
MIT © Omid Hashemzadeh
174+
MIT © Omid Hashemzadeh

docker-compose.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
1+
version: '3.9'
2+
13
services:
24
web:
35
build: .
46
command: /entrypoint.sh
57
volumes:
68
- .:/app
7-
ports:
8-
- "8000:8000"
9+
- static_volume:/app/staticfiles
10+
expose:
11+
- "8000" # Internal, not published to host
912
depends_on:
1013
- db
1114
- rabbitmq
1215
env_file:
1316
- .env
1417
environment:
15-
# Useful if override needed, else .env handles it
1618
POSTGRES_HOST: db
1719
CELERY_BROKER_URL: amqp://guest:guest@rabbitmq:5672//
1820

21+
nginx:
22+
image: nginx:alpine
23+
ports:
24+
- "80:80"
25+
depends_on:
26+
- web
27+
volumes:
28+
- static_volume:/usr/share/nginx/html/static
29+
- ./nginx.conf:/etc/nginx/nginx.conf:ro
1930

2031
db:
2132
image: postgres
@@ -42,6 +53,8 @@ services:
4253
env_file:
4354
- .env
4455
environment:
45-
# Useful if override needed, else .env handles it
4656
POSTGRES_HOST: db
4757
CELERY_BROKER_URL: amqp://guest:guest@rabbitmq:5672//
58+
59+
volumes:
60+
static_volume:

entrypoint.sh

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
#!/bin/bash
22

3+
4+
# Wait for PostgreSQL to be ready
5+
echo "⏳ Waiting for Postgres..."
6+
until nc -z $POSTGRES_HOST 5432; do
7+
sleep 1
8+
done
9+
echo "✅ Postgres is up!"
10+
311
echo "🚀 Running migrations..."
412
python manage.py migrate
513

14+
echo "📦 Collecting static files..."
15+
python manage.py collectstatic --noinput
16+
617
echo "🚀 Starting Gunicorn..."
7-
exec gunicorn taskflow_api.wsgi:application --bind 0.0.0.0:8000
18+
exec gunicorn taskflow_api.wsgi:application --bind 0.0.0.0:8000 --timeout 120

nginx.conf

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
worker_processes 1;
2+
events { worker_connections 1024; }
3+
4+
http {
5+
include mime.types;
6+
default_type application/octet-stream;
7+
8+
sendfile on;
9+
keepalive_timeout 65;
10+
11+
upstream django {
12+
server web:8000;
13+
}
14+
15+
server {
16+
listen 80;
17+
server_name localhost;
18+
19+
location /static/ {
20+
alias /usr/share/nginx/html/static/;
21+
}
22+
23+
location / {
24+
proxy_pass http://django;
25+
proxy_set_header Host $host;
26+
proxy_set_header X-Real-IP $remote_addr;
27+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
28+
proxy_set_header X-Forwarded-Proto $scheme;
29+
}
30+
}
31+
}

taskflow_api/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pathlib import Path
22
from decouple import config, Csv
3+
import os
34

45
# Build paths inside the project like this: BASE_DIR / 'subdir'.
56
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -90,6 +91,7 @@
9091
# https://docs.djangoproject.com/en/5.2/howto/static-files/
9192

9293
STATIC_URL = "static/"
94+
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
9395

9496
# Default primary key field type
9597
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

taskflow_api/urls.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from rest_framework import permissions
44
from drf_yasg.views import get_schema_view
55
from drf_yasg import openapi
6+
from django.conf import settings
7+
8+
from tasks.views import home_view
69

710
schema_view = get_schema_view(
811
openapi.Info(
@@ -23,3 +26,7 @@
2326
name="schema-swagger-ui",
2427
),
2528
]
29+
30+
if settings.DEBUG == "FALSE":
31+
# production home page
32+
urlpatterns.append(path("", home_view))

tasks/tests/test_api.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33
from django.urls import reverse
44
from datetime import date
55

6+
67
@pytest.mark.django_db
78
def test_create_task_api():
89
client = APIClient()
910
url = reverse("task-list") # Adjust if you use a different route name
10-
response = client.post(url, {
11-
"title": "API Task",
12-
"due_date": date.today(),
13-
}, format="json")
11+
response = client.post(
12+
url,
13+
{
14+
"title": "API Task",
15+
"due_date": date.today(),
16+
},
17+
format="json",
18+
)
1419
assert response.status_code == 201
1520
assert response.data["title"] == "API Task"

0 commit comments

Comments
 (0)