Assignment-1 :Docker

This commit is contained in:
Mohammed Niaz Khaleel Jameel 2026-04-01 06:53:48 +02:00
parent 292fe08c84
commit 5d26cf4329
29 changed files with 884 additions and 0 deletions

Binary file not shown.

25
z1/Dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN DJANGO_SETTINGS_MODULE=diary_app.settings_build python manage.py collectstatic --noinput
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8000
ENTRYPOINT ["/entrypoint.sh"]

0
z1/diary/__init__.py Normal file
View File

8
z1/diary/admin.py Normal file
View File

@ -0,0 +1,8 @@
from django.contrib import admin
from .models import Entry
@admin.register(Entry)
class EntryAdmin(admin.ModelAdmin):
list_display = ('title', 'user', 'mood', 'created_at')
list_filter = ('mood', 'user')
search_fields = ('title', 'content')

27
z1/diary/models.py Normal file
View File

@ -0,0 +1,27 @@
from django.db import models
from django.contrib.auth.models import User
MOOD_CHOICES = [
('happy', '😊 Happy'),
('sad', '😢 Sad'),
('anxious', '😰 Anxious'),
('calm', '😌 Calm'),
('excited', '🤩 Excited'),
('angry', '😠 Angry'),
('grateful', '🙏 Grateful'),
('tired', '😴 Tired'),
]
class Entry(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='entries')
title = models.CharField(max_length=200)
content = models.TextField()
mood = models.CharField(max_length=20, choices=MOOD_CHOICES, default='calm')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.title} ({self.created_at.strftime('%Y-%m-%d')})"

11
z1/diary/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.entry_list, name='entry_list'),
path('entry/new/', views.entry_create, name='entry_create'),
path('entry/<int:pk>/', views.entry_detail, name='entry_detail'),
path('entry/<int:pk>/edit/', views.entry_edit, name='entry_edit'),
path('entry/<int:pk>/del/', views.entry_delete, name='entry_delete'),
path('register/', views.register, name='register'),
]

86
z1/diary/views.py Normal file
View File

@ -0,0 +1,86 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from django.contrib import messages
from django.db.models import Q
from .models import Entry
@login_required
def entry_list(request):
query = request.GET.get('q', '')
mood = request.GET.get('mood', '')
entries = Entry.objects.filter(user=request.user)
if query:
entries = entries.filter(Q(title__icontains=query) | Q(content__icontains=query))
if mood:
entries = entries.filter(mood=mood)
return render(request, 'diary/entry_list.html', {
'entries': entries,
'query': query,
'mood_filter': mood,
'mood_choices': Entry.MOOD_CHOICES,
})
@login_required
def entry_detail(request, pk):
entry = get_object_or_404(Entry, pk=pk, user=request.user)
return render(request, 'diary/entry_detail.html', {'entry': entry})
@login_required
def entry_create(request):
if request.method == 'POST':
title = request.POST.get('title', '').strip()
content = request.POST.get('content', '').strip()
mood = request.POST.get('mood', 'calm')
if title and content:
Entry.objects.create(user=request.user, title=title, content=content, mood=mood)
messages.success(request, 'Entry created!')
return redirect('entry_list')
messages.error(request, 'Title and content are required.')
return render(request, 'diary/entry_form.html', {
'mood_choices': Entry.MOOD_CHOICES,
'action': 'Create',
})
@login_required
def entry_edit(request, pk):
entry = get_object_or_404(Entry, pk=pk, user=request.user)
if request.method == 'POST':
title = request.POST.get('title', '').strip()
content = request.POST.get('content', '').strip()
mood = request.POST.get('mood', 'calm')
if title and content:
entry.title = title
entry.content = content
entry.mood = mood
entry.save()
messages.success(request, 'Entry updated!')
return redirect('entry_detail', pk=pk)
messages.error(request, 'Title and content are required.')
return render(request, 'diary/entry_form.html', {
'entry': entry,
'mood_choices': Entry.MOOD_CHOICES,
'action': 'Edit',
})
@login_required
def entry_delete(request, pk):
entry = get_object_or_404(Entry, pk=pk, user=request.user)
if request.method == 'POST':
entry.delete()
messages.success(request, 'Entry deleted.')
return redirect('entry_list')
return render(request, 'diary/entry_confirm_delete.html', {'entry': entry})
def register(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
messages.success(request, f'Welcome, {user.username}!')
return redirect('entry_list')
else:
form = UserCreationForm()
return render(request, 'diary/register.html', {'form': form})

0
z1/diary_app/__init__.py Normal file
View File

72
z1/diary_app/settings.py Normal file
View File

@ -0,0 +1,72 @@
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
DEBUG = os.environ.get('DEBUG', 'True') == 'True'
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'diary',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'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 = 'diary_app.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'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',
],
},
},
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('POSTGRES_DB', 'diarydb'),
'USER': os.environ.get('POSTGRES_USER', 'diaryuser'),
'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'diarypass'),
'HOST': os.environ.get('DB_HOST', 'db'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/login/'

View File

@ -0,0 +1,4 @@
from diary_app.settings import *
DATABASES = {}
STATICFILES_DIRS = [BASE_DIR / 'static']

View File

@ -0,0 +1,193 @@
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:wght@300;400;500&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f0e8;
--surface: #fdfaf4;
--border: #ddd5c0;
--text: #1a1814;
--muted: #8c8070;
--accent: #c84b2f;
--accent-h: #a83a20;
--danger: #c0392b;
--radius: 6px;
--shadow: 0 1px 6px rgba(0,0,0,.08);
}
body {
font-family: 'DM Sans', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.65;
font-size: 16px;
font-weight: 300;
}
h1, h2, h3 {
font-family: 'Instrument Serif', serif;
font-weight: 400;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* ---- Navbar ---- */
.navbar {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 2.5rem;
height: 58px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.nav-brand {
font-family: 'Instrument Serif', serif;
font-size: 1.35rem;
color: var(--text);
letter-spacing: 0.01em;
}
.nav-brand:hover { text-decoration: none; }
.nav-links { display: flex; align-items: center; gap: .75rem; }
.nav-user { font-size: .85rem; color: var(--muted); font-weight: 300; }
/* ---- Container ---- */
.container { max-width: 880px; margin: 0 auto; padding: 2.5rem 2rem; }
/* ---- Buttons ---- */
.btn {
display: inline-flex; align-items: center; justify-content: center;
padding: .5rem 1.25rem;
border-radius: var(--radius);
font-family: 'DM Sans', sans-serif;
font-size: .875rem;
font-weight: 400;
cursor: pointer;
border: 1px solid transparent;
transition: all .15s;
text-decoration: none;
letter-spacing: 0.02em;
}
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-primary:hover { background: var(--accent-h); border-color: var(--accent-h); text-decoration: none; }
.btn-ghost { background: transparent; color: var(--text); border-color: var(--border); }
.btn-ghost:hover { background: var(--bg); text-decoration: none; }
.btn-danger { background: transparent; color: var(--danger); border-color: var(--danger); }
.btn-danger:hover { background: var(--danger); color: #fff; text-decoration: none; }
.btn-sm { padding: .35rem .9rem; font-size: .82rem; }
.btn-full { width: 100%; }
/* ---- Alerts ---- */
.alert { padding: .75rem 1.1rem; border-radius: var(--radius); margin-bottom: 1.25rem; font-size: .9rem; border-left: 3px solid; }
.alert-success { background: #f0f7f0; border-color: #5a9e6f; color: #2d6a40; }
.alert-error { background: #fdf0ee; border-color: var(--accent); color: var(--accent-h); }
/* ---- Page header ---- */
.page-header { margin-bottom: 2rem; border-bottom: 1px solid var(--border); padding-bottom: 1.25rem; }
.page-header h1 { font-size: 2.2rem; line-height: 1.2; }
.subtitle { color: var(--muted); font-size: .88rem; margin-top: .35rem; font-weight: 300; }
/* ---- Search bar ---- */
.search-form { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: 2rem; }
.input {
padding: .55rem .9rem;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: .9rem;
color: var(--text);
background: var(--surface);
outline: none;
transition: border-color .15s;
font-family: 'DM Sans', sans-serif;
font-weight: 300;
}
.input:focus { border-color: var(--accent); }
.search-form .input:first-child { flex: 1; min-width: 180px; }
.select-input { cursor: pointer; }
/* ---- Entries grid ---- */
.entries-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 1.25rem; }
.entry-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
display: block;
transition: box-shadow .15s, transform .15s;
color: var(--text);
}
.entry-card:hover {
box-shadow: var(--shadow);
transform: translateY(-2px);
text-decoration: none;
}
.entry-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .75rem; }
.mood-badge {
font-size: .75rem;
padding: .2rem .65rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 20px;
color: var(--muted);
font-weight: 400;
letter-spacing: 0.03em;
}
.mood-large { font-size: .88rem; padding: .3rem .85rem; }
.entry-date { font-size: .75rem; color: var(--muted); font-weight: 300; }
.entry-title {
font-family: 'Instrument Serif', serif;
font-size: 1.2rem;
font-weight: 400;
margin-bottom: .5rem;
line-height: 1.3;
}
.entry-excerpt { font-size: .875rem; color: var(--muted); line-height: 1.6; font-weight: 300; }
/* ---- Empty state ---- */
.empty-state { text-align: center; padding: 5rem 2rem; color: var(--muted); }
.empty-icon { font-size: 2.5rem; margin-bottom: 1rem; }
/* ---- Entry detail ---- */
.detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
.back-link { color: var(--muted); font-size: .88rem; font-weight: 300; }
.back-link:hover { color: var(--text); }
.detail-actions { display: flex; gap: .5rem; }
.entry-detail {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2.5rem;
}
.entry-meta { display: flex; align-items: center; gap: .85rem; margin-bottom: 1.25rem; }
.detail-title { font-size: 2.4rem; line-height: 1.2; margin-bottom: 2rem; }
.entry-content { font-size: 1.05rem; line-height: 1.85; color: var(--text); font-weight: 300; }
.entry-content p { margin-bottom: 1.1rem; }
/* ---- Forms ---- */
.form-page { max-width: 660px; }
.form-page h1 { font-size: 2rem; margin-bottom: 1.75rem; }
.diary-form { display: flex; flex-direction: column; gap: 1.35rem; }
.form-group { display: flex; flex-direction: column; gap: .45rem; }
.form-group label { font-size: .85rem; font-weight: 500; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.form-group .input { width: 100%; }
.textarea { resize: vertical; min-height: 240px; line-height: 1.7; }
.form-actions { display: flex; gap: .75rem; padding-top: .5rem; }
.field-error { font-size: .82rem; color: var(--danger); }
.field-hint { font-size: .82rem; color: var(--muted); font-weight: 300; }
/* ---- Auth ---- */
.auth-page { min-height: calc(100vh - 58px); display: flex; align-items: center; justify-content: center; padding: 2rem; }
.auth-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2.75rem;
width: 100%;
max-width: 420px;
}
.auth-card h1 { font-size: 1.9rem; margin-bottom: 1.75rem; }
.auth-footer { margin-top: 1.25rem; font-size: .875rem; text-align: center; color: var(--muted); font-weight: 300; }

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Diary{% endblock %}</title>
{% load static %}
<link rel="stylesheet" href="{% static 'diary/css/style.css' %}">
</head>
<body>
<nav class="navbar">
<a href="{% url 'entry_list' %}" class="nav-brand">📓 My Diary</a>
<div class="nav-links">
{% if user.is_authenticated %}
<span class="nav-user">Hello, <strong>{{ user.username }}</strong></span>
<a href="{% url 'entry_create' %}" class="btn btn-primary btn-sm">+ New Entry</a>
<a href="{% url 'logout' %}" class="btn btn-ghost btn-sm">Logout</a>
{% else %}
<a href="{% url 'login' %}" class="btn btn-ghost btn-sm">Login</a>
<a href="{% url 'register' %}" class="btn btn-primary btn-sm">Register</a>
{% endif %}
</div>
</nav>
<main class="container">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
</main>
</body>
</html>

View File

@ -0,0 +1,15 @@
{% extends 'diary/base.html' %}
{% block title %}Delete Entry{% endblock %}
{% block content %}
<div class="form-page">
<h1>Delete Entry</h1>
<p>Are you sure you want to delete <strong>"{{ entry.title }}"</strong>? This cannot be undone.</p>
<form method="post" class="diary-form">
{% csrf_token %}
<div class="form-actions">
<button type="submit" class="btn btn-danger">Yes, delete it</button>
<a href="{% url 'entry_detail' entry.pk %}" class="btn btn-ghost">Cancel</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends 'diary/base.html' %}
{% block title %}{{ entry.title }}{% endblock %}
{% block content %}
<div class="detail-header">
<a href="{% url 'entry_list' %}" class="back-link">← Back</a>
<div class="detail-actions">
<a href="{% url 'entry_edit' entry.pk %}" class="btn btn-primary btn-sm">Edit</a>
<a href="{% url 'entry_delete' entry.pk %}" class="btn btn-danger btn-sm">Delete</a>
</div>
</div>
<article class="entry-detail">
<div class="entry-meta">
<span class="mood-badge mood-large">{{ entry.get_mood_display }}</span>
<span class="entry-date">{{ entry.created_at|date:"F j, Y — H:i" }}</span>
{% if entry.updated_at != entry.created_at %}
<span class="entry-date">(edited {{ entry.updated_at|date:"M d" }})</span>
{% endif %}
</div>
<h1 class="detail-title">{{ entry.title }}</h1>
<div class="entry-content">{{ entry.content|linebreaks }}</div>
</article>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends 'diary/base.html' %}
{% block title %}{{ action }} Entry{% endblock %}
{% block content %}
<div class="form-page">
<h1>{{ action }} Entry</h1>
<form method="post" class="diary-form">
{% csrf_token %}
<div class="form-group">
<label for="title">Title</label>
<input type="text" id="title" name="title"
value="{{ entry.title|default:'' }}"
placeholder="Give your entry a title…"
class="input" required>
</div>
<div class="form-group">
<label for="mood">Mood</label>
<select id="mood" name="mood" class="input">
{% for val, label in mood_choices %}
<option value="{{ val }}"
{% if entry.mood == val or not entry and val == 'calm' %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="content">Write your thoughts…</label>
<textarea id="content" name="content" rows="12"
placeholder="What's on your mind today?"
class="input textarea" required>{{ entry.content|default:'' }}</textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{ action }} Entry</button>
<a href="{% if entry %}{% url 'entry_detail' entry.pk %}{% else %}{% url 'entry_list' %}{% endif %}"
class="btn btn-ghost">Cancel</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'diary/base.html' %}
{% block title %}My Diary — All Entries{% endblock %}
{% block content %}
<div class="page-header">
<h1>My Diary</h1>
<p class="subtitle">{{ entries.count }} entr{{ entries.count|pluralize:"y,ies" }}</p>
</div>
<div class="search-bar">
<form method="get" class="search-form">
<input type="text" name="q" value="{{ query }}" placeholder="Search entries…" class="input">
<select name="mood" class="input select-input">
<option value="">All moods</option>
{% for val, label in mood_choices %}
<option value="{{ val }}" {% if mood_filter == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-primary">Search</button>
{% if query or mood_filter %}
<a href="{% url 'entry_list' %}" class="btn btn-ghost">Clear</a>
{% endif %}
</form>
</div>
{% if entries %}
<div class="entries-grid">
{% for entry in entries %}
<a href="{% url 'entry_detail' entry.pk %}" class="entry-card">
<div class="entry-card-header">
<span class="mood-badge">{{ entry.get_mood_display }}</span>
<span class="entry-date">{{ entry.created_at|date:"M d, Y" }}</span>
</div>
<h2 class="entry-title">{{ entry.title }}</h2>
<p class="entry-excerpt">{{ entry.content|truncatechars:120 }}</p>
</a>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<p class="empty-icon">📓</p>
<p>No entries yet. <a href="{% url 'entry_create' %}">Write your first one!</a></p>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends 'diary/base.html' %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="auth-page">
<div class="auth-card">
<h1>Welcome back 📓</h1>
<form method="post" class="diary-form">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
<input type="{{ field.field.widget.input_type }}"
id="{{ field.id_for_label }}"
name="{{ field.html_name }}"
class="input"
{% if field.field.required %}required{% endif %}>
{% if field.errors %}
<span class="field-error">{{ field.errors|join:', ' }}</span>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary btn-full">Login</button>
</form>
<p class="auth-footer">No account? <a href="{% url 'register' %}">Register here</a></p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends 'diary/base.html' %}
{% block title %}Register{% endblock %}
{% block content %}
<div class="auth-page">
<div class="auth-card">
<h1>Create an account 📓</h1>
<form method="post" class="diary-form">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
<input type="{{ field.field.widget.input_type }}"
id="{{ field.id_for_label }}"
name="{{ field.html_name }}"
class="input"
{% if field.field.required %}required{% endif %}>
{% if field.help_text %}
<span class="field-hint">{{ field.help_text }}</span>
{% endif %}
{% if field.errors %}
<span class="field-error">{{ field.errors|join:', ' }}</span>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary btn-full">Register</button>
</form>
<p class="auth-footer">Already have an account? <a href="{% url 'login' %}">Login</a></p>
</div>
</div>
{% endblock %}

10
z1/diary_app/urls.py Normal file
View File

@ -0,0 +1,10 @@
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', auth_views.LoginView.as_view(template_name='diary/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('', include('diary.urls')),
]

5
z1/diary_app/wsgi.py Normal file
View File

@ -0,0 +1,5 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_app.settings')
application = get_wsgi_application()

63
z1/docker-compose.yaml Normal file
View File

@ -0,0 +1,63 @@
services:
db:
image: postgres:15-alpine
container_name: diary-db
restart: unless-stopped
environment:
POSTGRES_DB: diarydb
POSTGRES_USER: diaryuser
POSTGRES_PASSWORD: diarypass
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- diary_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U diaryuser -d diarydb"]
interval: 5s
timeout: 5s
retries: 10
app:
build: .
image: diary-app:latest
container_name: diary-app
restart: unless-stopped
environment:
DJANGO_SETTINGS_MODULE: diary_app.settings
POSTGRES_DB: diarydb
POSTGRES_USER: diaryuser
POSTGRES_PASSWORD: diarypass
DB_HOST: db
DB_PORT: "5432"
SECRET_KEY: change-me-in-production-use-a-long-random-string
DEBUG: "False"
volumes:
- diary_static:/app/staticfiles
networks:
- diary_network
depends_on:
db:
condition: service_healthy
nginx:
image: nginx:alpine
container_name: diary-nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- diary_static:/app/staticfiles:ro
networks:
- diary_network
depends_on:
- app
volumes:
postgres_data:
diary_static:
networks:
diary_network:
driver: bridge

30
z1/entrypoint.sh Normal file
View File

@ -0,0 +1,30 @@
#!/bin/sh
set -e
echo "Waiting for PostgreSQL to be ready..."
until python -c "
import psycopg2, os, sys
try:
psycopg2.connect(
dbname=os.environ.get('POSTGRES_DB','diarydb'),
user=os.environ.get('POSTGRES_USER','diaryuser'),
password=os.environ.get('POSTGRES_PASSWORD','diarypass'),
host=os.environ.get('DB_HOST','db'),
port=os.environ.get('DB_PORT','5432')
)
except Exception:
sys.exit(1)
"; do
echo " DB not ready yet, retrying in 2s..."
sleep 2
done
echo "PostgreSQL is ready."
echo "Running migrations..."
python manage.py migrate --noinput
echo "Starting Gunicorn..."
exec gunicorn diary_app.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 2 \
--timeout 120

13
z1/manage.py Normal file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env python
import os
import sys
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_app.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?"
) from exc
execute_from_command_line(sys.argv)

21
z1/nginx/nginx.conf Normal file
View File

@ -0,0 +1,21 @@
upstream django {
server app:8000;
}
server {
listen 80;
server_name localhost;
location /static/ {
alias /app/staticfiles/;
}
location / {
proxy_pass http://django;
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;
}
}

23
z1/prepare-app.sh Normal file
View File

@ -0,0 +1,23 @@
#!/bin/bash
set -e
echo "Preparing app..."
if docker compose version > /dev/null 2>&1; then
echo " Using docker compose to build image..."
docker compose build
else
echo " Using plain docker to build image..."
docker build -t diary-app:latest .
docker network create diary_network 2>/dev/null || echo " Network diary_network already exists, skipping."
docker volume create postgres_data 2>/dev/null || echo " Volume postgres_data already exists, skipping."
docker volume create diary_static 2>/dev/null || echo " Volume diary_static already exists, skipping."
fi
echo ""
echo "Preparation complete."
echo " Image : diary-app:latest"
echo " Network: diary_network"
echo " Volumes: postgres_data, diary_static"
echo ""
echo "Run ./start-app.sh to start the application."

17
z1/remove-app.sh Normal file
View File

@ -0,0 +1,17 @@
#!/bin/bash
echo "Removing app..."
if docker compose version > /dev/null 2>&1; then
docker compose down --volumes --rmi all
else
docker stop diary-nginx diary-app diary-db 2>/dev/null || true
docker rm diary-nginx diary-app diary-db 2>/dev/null || true
docker rmi diary-app:latest 2>/dev/null && echo " Removed image: diary-app:latest" || echo " Image not found, skipping."
docker volume rm postgres_data 2>/dev/null && echo " Removed volume: postgres_data" || echo " Volume not found, skipping."
docker volume rm diary_static 2>/dev/null && echo " Removed volume: diary_static" || echo " Volume not found, skipping."
docker network rm diary_network 2>/dev/null && echo " Removed network: diary_network" || echo " Network not found, skipping."
fi
echo ""
echo "Removed app."

3
z1/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
Django==4.2.11
psycopg2-binary==2.9.9
gunicorn==21.2.0

49
z1/start-app.sh Normal file
View File

@ -0,0 +1,49 @@
#!/bin/bash
set -e
echo "Running app..."
if docker compose version > /dev/null 2>&1; then
docker compose up -d
else
docker run -d \
--name diary-db \
--network diary_network \
--restart unless-stopped \
-e POSTGRES_DB=diarydb \
-e POSTGRES_USER=diaryuser \
-e POSTGRES_PASSWORD=diarypass \
-v postgres_data:/var/lib/postgresql/data \
postgres:15-alpine
docker run -d \
--name diary-app \
--network diary_network \
--restart unless-stopped \
-e DJANGO_SETTINGS_MODULE=diary_app.settings \
-e POSTGRES_DB=diarydb \
-e POSTGRES_USER=diaryuser \
-e POSTGRES_PASSWORD=diarypass \
-e DB_HOST=diary-db \
-e DB_PORT=5432 \
-e SECRET_KEY=change-me-in-production-use-a-long-random-string \
-e DEBUG=False \
-v diary_static:/app/staticfiles \
diary-app:latest
docker run -d \
--name diary-nginx \
--network diary_network \
--restart unless-stopped \
-p 80:80 \
-v "$(pwd)/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro" \
-v diary_static:/app/staticfiles:ro \
nginx:alpine
fi
echo ""
echo "All containers started."
echo "The app is available at http://localhost"
echo ""
echo "Note: First startup may take 10-15 seconds while the"
echo " database initialises and migrations run."

16
z1/stop-app.sh Normal file
View File

@ -0,0 +1,16 @@
#!/bin/bash
echo "Stopping app..."
if docker compose version > /dev/null 2>&1; then
docker compose down
else
docker stop diary-nginx 2>/dev/null && echo " Stopped diary-nginx" || echo " diary-nginx was not running"
docker stop diary-app 2>/dev/null && echo " Stopped diary-app" || echo " diary-app was not running"
docker stop diary-db 2>/dev/null && echo " Stopped diary-db" || echo " diary-db was not running"
docker rm diary-nginx diary-app diary-db 2>/dev/null || true
fi
echo ""
echo "App stopped. All data is preserved in the postgres_data volume."
echo "Run ./start-app.sh to start it again."