Initial commit

This commit is contained in:
pradeeppraveen 2026-04-01 09:38:54 +02:00
commit b9bb60f17e
13 changed files with 341 additions and 0 deletions

107
README.md Normal file
View File

@ -0,0 +1,107 @@
# Notes App — Docker Assignment
## What the application does
A simple **Notes** web application where users can create, view, and delete short text notes. Notes are stored persistently in a PostgreSQL database. The app is accessible via a web browser.
## Services
| Container | Image | Port | Description |
|---|---|---|---|
| `app_frontend` | custom (Nginx) | `8080→80` | Serves the static HTML/JS frontend and proxies `/api/` requests to the backend |
| `app_backend` | custom (Flask/Python) | internal `5000` | REST API for CRUD operations on notes |
| `app_db` | `postgres:16-alpine` | internal `5432` | Relational database storing notes persistently |
| `app_adminer` | `adminer:4` | `8081→8080` | Web UI for browsing and managing the PostgreSQL database |
## Networks
| Network | Purpose |
|---|---|
| `frontend_net` | Connects the Nginx frontend container (externally reachable) |
| `backend_net` | Internal network connecting frontend→backend→database and Adminer→database |
The database is only on `backend_net` and is never directly exposed to the host.
## Volumes
| Volume | Used by | Purpose |
|---|---|---|
| `postgres_data` | `app_db` | Persists PostgreSQL data across container restarts and stops |
## Container configuration
- **app_db**: configured via environment variables (`POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`). The `db/init.sql` file is mounted read-only into `/docker-entrypoint-initdb.d/` and runs once on first startup to create the `notes` table and insert seed data.
- **app_backend**: receives `DATABASE_URL` as an environment variable pointing to the `db` service. Built from `./backend/Dockerfile` using Python 3.12-slim with Flask and psycopg2.
- **app_frontend**: built from `./frontend/Dockerfile` using Nginx 1.27-alpine. The custom `nginx.conf` proxies all `/api/` requests to `app_backend:5000` and serves `index.html` for all other paths.
- **app_adminer**: uses the official Adminer image with no extra configuration. Reachable on port `8081`.
- All containers use `restart: on-failure`.
## Requirements
- Linux OS
- Docker Engine ≥ 24 with the Compose plugin (`docker compose`)
## Instructions
### Prepare (build images)
```bash
./prepare-app.sh
```
### Start
```bash
./start-app.sh
```
### Stop (data is preserved)
```bash
./stop-app.sh
```
### Remove everything
```bash
./remove-app.sh
```
## Viewing the application
| URL | Description |
|---|---|
| http://localhost:8080 | Notes web application |
| http://localhost:8081 | Adminer — log in with Server: `db`, User: `appuser`, Password: `apppassword`, Database: `appdb` |
## Example workflow
```bash
./prepare-app.sh
# Preparing app...
./start-app.sh
# Running app...
# The app is available at http://localhost:8080
# Adminer (DB UI) is available at http://localhost:8081
# Open browser, add/delete notes
./stop-app.sh
# Stopping app...
# App stopped. Data in volumes is preserved.
./start-app.sh
# Notes are still there — volume was preserved
./remove-app.sh
# Removing app.
```
## Resources used
- [Docker documentation](https://docs.docker.com/)
- [Flask documentation](https://flask.palletsprojects.com/)
- [Nginx documentation](https://nginx.org/en/docs/)
- [PostgreSQL Docker image](https://hub.docker.com/_/postgres)
- [Adminer Docker image](https://hub.docker.com/_/adminer)
## Use of artificial intelligence
This project was created with the assistance of **Kiro AI** (kiro-cli chat agent). The AI generated the initial structure of all files including the Docker Compose configuration, Flask application, Nginx configuration, frontend HTML/JS, and shell scripts. All generated code was reviewed and understood by the author. The AI was used as a coding assistant, not as a replacement for understanding the solution.

11
backend/Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]

37
backend/app.py Normal file
View File

@ -0,0 +1,37 @@
import os
import psycopg2
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
def get_conn():
return psycopg2.connect(os.environ["DATABASE_URL"])
@app.route("/api/notes", methods=["GET"])
def get_notes():
with get_conn() as conn, conn.cursor() as cur:
cur.execute("SELECT id, content, created_at FROM notes ORDER BY created_at DESC")
rows = cur.fetchall()
return jsonify([{"id": r[0], "content": r[1], "created_at": r[2].isoformat()} for r in rows])
@app.route("/api/notes", methods=["POST"])
def add_note():
data = request.get_json()
content = (data or {}).get("content", "").strip()
if not content:
abort(400, "content required")
with get_conn() as conn, conn.cursor() as cur:
cur.execute("INSERT INTO notes (content) VALUES (%s) RETURNING id, content, created_at", (content,))
row = cur.fetchone()
conn.commit()
return jsonify({"id": row[0], "content": row[1], "created_at": row[2].isoformat()}), 201
@app.route("/api/notes/<int:note_id>", methods=["DELETE"])
def delete_note(note_id):
with get_conn() as conn, conn.cursor() as cur:
cur.execute("DELETE FROM notes WHERE id = %s", (note_id,))
conn.commit()
return "", 204
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)

2
backend/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
flask==3.0.3
psycopg2-binary==2.9.9

9
db/init.sql Normal file
View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS notes (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO notes (content) VALUES
('Welcome to the Notes App!'),
('Add, view, and delete notes using the web interface.');

55
docker-compose.yaml Normal file
View File

@ -0,0 +1,55 @@
version: "3.9"
networks:
frontend_net:
backend_net:
volumes:
postgres_data:
services:
db:
image: postgres:16-alpine
container_name: app_db
restart: on-failure
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppassword
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- backend_net
backend:
build: ./backend
container_name: app_backend
restart: on-failure
environment:
DATABASE_URL: postgresql://appuser:apppassword@db:5432/appdb
depends_on:
- db
networks:
- backend_net
frontend:
build: ./frontend
container_name: app_frontend
restart: on-failure
ports:
- "8080:80"
depends_on:
- backend
networks:
- frontend_net
- backend_net
adminer:
image: adminer:4
container_name: app_adminer
restart: on-failure
ports:
- "8081:8080"
networks:
- backend_net

6
frontend/Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80

65
frontend/index.html Normal file
View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Notes App</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 0 16px; }
h1 { color: #333; }
textarea { width: 100%; height: 80px; box-sizing: border-box; }
button { margin-top: 8px; padding: 8px 16px; cursor: pointer; }
ul { list-style: none; padding: 0; }
li { display: flex; justify-content: space-between; align-items: flex-start;
border-bottom: 1px solid #eee; padding: 10px 0; }
li span { flex: 1; white-space: pre-wrap; }
li small { color: #999; font-size: 0.8em; display: block; }
.del { background: #e55; color: #fff; border: none; border-radius: 4px;
padding: 4px 10px; cursor: pointer; margin-left: 12px; }
#status { color: red; margin-top: 8px; }
</style>
</head>
<body>
<h1>📝 Notes App</h1>
<textarea id="content" placeholder="Write a note..."></textarea><br/>
<button onclick="addNote()">Add Note</button>
<p id="status"></p>
<ul id="notes"></ul>
<script>
async function loadNotes() {
const res = await fetch('/api/notes');
const notes = await res.json();
const ul = document.getElementById('notes');
ul.innerHTML = notes.map(n => `
<li>
<div><span>${escHtml(n.content)}</span><small>${n.created_at}</small></div>
<button class="del" onclick="deleteNote(${n.id})"></button>
</li>`).join('');
}
async function addNote() {
const ta = document.getElementById('content');
const content = ta.value.trim();
if (!content) return;
const res = await fetch('/api/notes', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({content})
});
if (res.ok) { ta.value = ''; loadNotes(); }
else document.getElementById('status').textContent = 'Error adding note.';
}
async function deleteNote(id) {
await fetch('/api/notes/' + id, {method: 'DELETE'});
loadNotes();
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
loadNotes();
</script>
</body>
</html>

14
frontend/nginx.conf Normal file
View File

@ -0,0 +1,14 @@
server {
listen 80;
location /api/ {
proxy_pass http://backend:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
root /usr/share/nginx/html;
index index.html;
}
}

8
prepare-app.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
echo "Preparing app..."
docker compose build
echo "Done. Run ./start-app.sh to start the application."

8
remove-app.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
echo "Removing app..."
docker compose down --volumes --rmi local
echo "Removed app."

11
start-app.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
echo "Running app..."
docker compose up -d
echo ""
echo "The app is available at http://localhost:8080"
echo "Adminer (DB UI) is available at http://localhost:8081"
echo " Server: db | User: appuser | Password: apppassword | Database: appdb"

8
stop-app.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
echo "Stopping app..."
docker compose stop
echo "App stopped. Data in volumes is preserved."