commit b9bb60f17e5a8281046de999b40ad52d377884a9 Author: pradeeppraveen Date: Wed Apr 1 09:38:54 2026 +0200 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..930b768 --- /dev/null +++ b/README.md @@ -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. diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..3bcb3fc --- /dev/null +++ b/backend/Dockerfile @@ -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"] diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..97dd7f9 --- /dev/null +++ b/backend/app.py @@ -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/", 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) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..73c0be5 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,2 @@ +flask==3.0.3 +psycopg2-binary==2.9.9 diff --git a/db/init.sql b/db/init.sql new file mode 100644 index 0000000..c1a9fbb --- /dev/null +++ b/db/init.sql @@ -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.'); diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..7b03672 --- /dev/null +++ b/docker-compose.yaml @@ -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 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..bb81d19 --- /dev/null +++ b/frontend/Dockerfile @@ -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 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..07a9a6a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,65 @@ + + + + + Notes App + + + +

📝 Notes App

+
+ +

+ + + + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..c0cb0ff --- /dev/null +++ b/frontend/nginx.conf @@ -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; + } +} diff --git a/prepare-app.sh b/prepare-app.sh new file mode 100755 index 0000000..9fed5f3 --- /dev/null +++ b/prepare-app.sh @@ -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." diff --git a/remove-app.sh b/remove-app.sh new file mode 100755 index 0000000..542a6aa --- /dev/null +++ b/remove-app.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e + +echo "Removing app..." + +docker compose down --volumes --rmi local + +echo "Removed app." diff --git a/start-app.sh b/start-app.sh new file mode 100755 index 0000000..dcb241b --- /dev/null +++ b/start-app.sh @@ -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" diff --git a/stop-app.sh b/stop-app.sh new file mode 100755 index 0000000..78ea354 --- /dev/null +++ b/stop-app.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e + +echo "Stopping app..." + +docker compose stop + +echo "App stopped. Data in volumes is preserved."