Initial commit
This commit is contained in:
commit
b9bb60f17e
107
README.md
Normal file
107
README.md
Normal 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
11
backend/Dockerfile
Normal 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
37
backend/app.py
Normal 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
2
backend/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
flask==3.0.3
|
||||
psycopg2-binary==2.9.9
|
||||
9
db/init.sql
Normal file
9
db/init.sql
Normal 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
55
docker-compose.yaml
Normal 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
6
frontend/Dockerfile
Normal 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
65
frontend/index.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
loadNotes();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
14
frontend/nginx.conf
Normal file
14
frontend/nginx.conf
Normal 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
8
prepare-app.sh
Executable 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
8
remove-app.sh
Executable 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
11
start-app.sh
Executable 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
8
stop-app.sh
Executable 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."
|
||||
Loading…
Reference in New Issue
Block a user