Task Manager Docker web application
3-service Docker app: Nginx frontend, Flask REST API backend, PostgreSQL database. Includes lifecycle scripts (prepare, start, stop, remove), docker-compose.yaml, and documentation.
This commit is contained in:
commit
8dc74a1062
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
docs/superpowers/
|
||||||
207
README.md
Normal file
207
README.md
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
# Task Manager - Docker Web Application
|
||||||
|
|
||||||
|
A simple task manager web application deployed as a multi-container Docker system. Users can create, complete, and delete tasks through a web interface.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Linux** with Docker installed (Docker Engine 20.10+)
|
||||||
|
- **Docker Compose** v2 (optional, for `docker compose` deployment)
|
||||||
|
- Ports **80** must be available on the host machine
|
||||||
|
|
||||||
|
## Application Description
|
||||||
|
|
||||||
|
The Task Manager is a web-based CRUD application for managing personal tasks. It consists of three services working together:
|
||||||
|
|
||||||
|
- A **frontend** web interface served by Nginx where users interact with the application
|
||||||
|
- A **backend** REST API built with Flask (Python) that handles business logic
|
||||||
|
- A **PostgreSQL database** that stores task data persistently
|
||||||
|
|
||||||
|
Users can:
|
||||||
|
- Add new tasks
|
||||||
|
- Mark tasks as completed (toggle checkbox)
|
||||||
|
- Delete tasks
|
||||||
|
- View all tasks in a list sorted by creation date
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser (port 80)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+--------+ +-------+ +------------+
|
||||||
|
| Nginx | ----> | Flask | ----> | PostgreSQL |
|
||||||
|
| :80 | API | :5000 | SQL | :5432 |
|
||||||
|
+--------+ proxy +-------+ +------------+
|
||||||
|
static REST API persistent
|
||||||
|
files (gunicorn) volume
|
||||||
|
```
|
||||||
|
|
||||||
|
## Virtual Networks
|
||||||
|
|
||||||
|
| Network Name | Driver | Purpose |
|
||||||
|
|-------------------|--------|------------------------------------------------------|
|
||||||
|
| taskapp-network | bridge | Connects all 3 containers so they can communicate |
|
||||||
|
|
||||||
|
All containers are attached to `taskapp-network`. Only Nginx exposes a port (80) to the host. Flask and PostgreSQL are accessible only within the Docker network.
|
||||||
|
|
||||||
|
## Named Volumes
|
||||||
|
|
||||||
|
| Volume Name | Mount Point | Purpose |
|
||||||
|
|----------------|------------------------------------|----------------------------------|
|
||||||
|
| taskapp-pgdata | /var/lib/postgresql/data (in db) | Persists database data across container restarts and stops |
|
||||||
|
|
||||||
|
Stopping and restarting the application preserves all task data thanks to this volume.
|
||||||
|
|
||||||
|
## Containers
|
||||||
|
|
||||||
|
### 1. taskapp-nginx (Frontend)
|
||||||
|
|
||||||
|
- **Image:** Custom, built from `nginx:alpine`
|
||||||
|
- **Port:** 80 (host) -> 80 (container)
|
||||||
|
- **Role:** Serves static HTML/CSS/JS files and reverse-proxies `/api/*` requests to the Flask backend
|
||||||
|
- **Restart policy:** `unless-stopped`
|
||||||
|
- **Configuration:** Custom `nginx.conf` with `proxy_pass` directive for API routing
|
||||||
|
|
||||||
|
### 2. taskapp-flask (Backend)
|
||||||
|
|
||||||
|
- **Image:** Custom, built from `python:3.12-slim`
|
||||||
|
- **Port:** 5000 (internal only, not exposed to host)
|
||||||
|
- **Role:** REST API server handling task CRUD operations
|
||||||
|
- **Restart policy:** `unless-stopped`
|
||||||
|
- **Configuration:** Environment variables for database connection (`DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`)
|
||||||
|
- **WSGI server:** Gunicorn with 2 workers
|
||||||
|
- **Auto-initialization:** Creates the `tasks` table on startup if it does not exist
|
||||||
|
|
||||||
|
### 3. taskapp-db (Database)
|
||||||
|
|
||||||
|
- **Image:** `postgres:15` (from Docker Hub)
|
||||||
|
- **Port:** 5432 (internal only, not exposed to host)
|
||||||
|
- **Role:** Stores task data (id, title, completed status, creation timestamp)
|
||||||
|
- **Restart policy:** `unless-stopped`
|
||||||
|
- **Configuration:** Environment variables (`POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`)
|
||||||
|
- **Volume:** `taskapp-pgdata` mounted at `/var/lib/postgresql/data`
|
||||||
|
|
||||||
|
## Container Configuration Details
|
||||||
|
|
||||||
|
- **Nginx** is configured via `frontend/nginx.conf` which sets up static file serving and reverse proxy rules
|
||||||
|
- **Flask** reads database credentials from environment variables passed at container runtime
|
||||||
|
- **PostgreSQL** is configured via standard Postgres environment variables; data is stored on a named volume
|
||||||
|
|
||||||
|
## Usage Instructions
|
||||||
|
|
||||||
|
### Prepare the Application
|
||||||
|
|
||||||
|
Build images and create Docker resources:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./prepare-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start the Application
|
||||||
|
|
||||||
|
Run all containers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
Starting app...
|
||||||
|
App is running!
|
||||||
|
The app is available at http://localhost:80
|
||||||
|
```
|
||||||
|
|
||||||
|
### View in Web Browser
|
||||||
|
|
||||||
|
Open your web browser and navigate to:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:80
|
||||||
|
```
|
||||||
|
|
||||||
|
You will see the Task Manager interface where you can add, complete, and delete tasks.
|
||||||
|
|
||||||
|
### Stop the Application
|
||||||
|
|
||||||
|
Stop all containers (data is preserved):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./stop-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove the Application
|
||||||
|
|
||||||
|
Remove all containers, images, networks, and volumes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./remove-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative: Docker Compose
|
||||||
|
|
||||||
|
You can also use Docker Compose instead of the shell scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# Stop (preserves data)
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Remove everything including volumes
|
||||||
|
docker compose down -v --rmi all
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|-------------------|------------------------|
|
||||||
|
| GET | /api/tasks | List all tasks |
|
||||||
|
| POST | /api/tasks | Create a new task |
|
||||||
|
| PUT | /api/tasks/:id | Toggle task completion |
|
||||||
|
| DELETE | /api/tasks/:id | Delete a task |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── backend/
|
||||||
|
│ ├── Dockerfile # Python/Flask image definition
|
||||||
|
│ ├── requirements.txt # Python dependencies
|
||||||
|
│ └── app.py # Flask REST API application
|
||||||
|
├── frontend/
|
||||||
|
│ ├── Dockerfile # Nginx image definition
|
||||||
|
│ ├── nginx.conf # Nginx configuration (static files + reverse proxy)
|
||||||
|
│ ├── index.html # Main HTML page
|
||||||
|
│ ├── style.css # Styles
|
||||||
|
│ └── app.js # Frontend JavaScript (fetch API calls)
|
||||||
|
├── docker-compose.yaml # Docker Compose configuration
|
||||||
|
├── prepare-app.sh # Script to build images and create resources
|
||||||
|
├── start-app.sh # Script to start all containers
|
||||||
|
├── stop-app.sh # Script to stop all containers
|
||||||
|
├── remove-app.sh # Script to remove all traces of the app
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [Docker Documentation](https://docs.docker.com/)
|
||||||
|
- [Nginx Docker Image](https://hub.docker.com/_/nginx)
|
||||||
|
- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres)
|
||||||
|
- [Python Docker Image](https://hub.docker.com/_/python)
|
||||||
|
- [Flask Documentation](https://flask.palletsprojects.com/)
|
||||||
|
- [Gunicorn Documentation](https://gunicorn.org/)
|
||||||
|
- [Nginx Reverse Proxy Guide](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
|
||||||
|
|
||||||
|
## Use of Artificial Intelligence
|
||||||
|
|
||||||
|
This application was designed and implemented with the assistance of **Claude** (Anthropic), an AI assistant. Claude was used for:
|
||||||
|
|
||||||
|
- Designing the application architecture and service composition
|
||||||
|
- Writing application source code (Python/Flask backend, HTML/CSS/JS frontend)
|
||||||
|
- Writing Dockerfiles and Docker Compose configuration
|
||||||
|
- Writing shell scripts for application lifecycle management
|
||||||
|
- Writing this documentation
|
||||||
|
|
||||||
|
**AI agent used:** Claude Opus 4.6 (Anthropic) via Claude Code CLI
|
||||||
12
backend/Dockerfile
Normal file
12
backend/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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 ["sh", "-c", "python -c 'from app import init_db; init_db()' && gunicorn --bind 0.0.0.0:5000 --workers 2 app:app"]
|
||||||
110
backend/app.py
Normal file
110
backend/app.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": os.environ.get("DB_HOST", "db"),
|
||||||
|
"database": os.environ.get("DB_NAME", "taskapp"),
|
||||||
|
"user": os.environ.get("DB_USER", "taskapp"),
|
||||||
|
"password": os.environ.get("DB_PASSWORD", "taskapp123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
return psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
"""Wait for PostgreSQL and create the tasks table if it doesn't exist."""
|
||||||
|
for attempt in range(30):
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS tasks (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
print("Database initialized.")
|
||||||
|
return
|
||||||
|
except psycopg2.OperationalError:
|
||||||
|
print(f"Waiting for database... (attempt {attempt + 1}/30)")
|
||||||
|
time.sleep(2)
|
||||||
|
raise RuntimeError("Could not connect to the database after 30 attempts.")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/tasks", methods=["GET"])
|
||||||
|
def get_tasks():
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT * FROM tasks ORDER BY created_at DESC")
|
||||||
|
tasks = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify(tasks)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/tasks", methods=["POST"])
|
||||||
|
def create_task():
|
||||||
|
data = request.get_json()
|
||||||
|
title = data.get("title", "").strip()
|
||||||
|
if not title:
|
||||||
|
return jsonify({"error": "Title is required"}), 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO tasks (title) VALUES (%s) RETURNING *",
|
||||||
|
(title,),
|
||||||
|
)
|
||||||
|
task = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify(task), 201
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/tasks/<int:task_id>", methods=["PUT"])
|
||||||
|
def update_task(task_id):
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE tasks SET completed = NOT completed WHERE id = %s RETURNING *",
|
||||||
|
(task_id,),
|
||||||
|
)
|
||||||
|
task = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
if task is None:
|
||||||
|
return jsonify({"error": "Task not found"}), 404
|
||||||
|
return jsonify(task)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/tasks/<int:task_id>", methods=["DELETE"])
|
||||||
|
def delete_task(task_id):
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("DELETE FROM tasks WHERE id = %s RETURNING id", (task_id,))
|
||||||
|
deleted = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
if deleted is None:
|
||||||
|
return jsonify({"error": "Task not found"}), 404
|
||||||
|
return jsonify({"result": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_db()
|
||||||
|
app.run(host="0.0.0.0", port=5000)
|
||||||
3
backend/requirements.txt
Normal file
3
backend/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
flask==3.1.1
|
||||||
|
psycopg2-binary==2.9.10
|
||||||
|
gunicorn==23.0.0
|
||||||
47
docker-compose.yaml
Normal file
47
docker-compose.yaml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: taskapp-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: taskapp
|
||||||
|
POSTGRES_USER: taskapp
|
||||||
|
POSTGRES_PASSWORD: taskapp123
|
||||||
|
volumes:
|
||||||
|
- taskapp-pgdata:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- taskapp-network
|
||||||
|
|
||||||
|
flask:
|
||||||
|
build: ./backend
|
||||||
|
image: taskapp-backend
|
||||||
|
container_name: taskapp-flask
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_NAME: taskapp
|
||||||
|
DB_USER: taskapp
|
||||||
|
DB_PASSWORD: taskapp123
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
networks:
|
||||||
|
- taskapp-network
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
build: ./frontend
|
||||||
|
image: taskapp-frontend
|
||||||
|
container_name: taskapp-nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- flask
|
||||||
|
networks:
|
||||||
|
- taskapp-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
taskapp-pgdata:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
taskapp-network:
|
||||||
|
driver: bridge
|
||||||
8
frontend/Dockerfile
Normal file
8
frontend/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY index.html /usr/share/nginx/html/
|
||||||
|
COPY style.css /usr/share/nginx/html/
|
||||||
|
COPY app.js /usr/share/nginx/html/
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
72
frontend/app.js
Normal file
72
frontend/app.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
const taskList = document.getElementById("task-list");
|
||||||
|
const taskForm = document.getElementById("task-form");
|
||||||
|
const taskInput = document.getElementById("task-input");
|
||||||
|
const emptyMsg = document.getElementById("empty-msg");
|
||||||
|
|
||||||
|
async function fetchTasks() {
|
||||||
|
const res = await fetch("/api/tasks");
|
||||||
|
const tasks = await res.json();
|
||||||
|
renderTasks(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTasks(tasks) {
|
||||||
|
taskList.innerHTML = "";
|
||||||
|
emptyMsg.style.display = tasks.length === 0 ? "block" : "none";
|
||||||
|
|
||||||
|
tasks.forEach(function (task) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "task-item" + (task.completed ? " completed" : "");
|
||||||
|
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.className = "task-checkbox";
|
||||||
|
checkbox.checked = task.completed;
|
||||||
|
checkbox.addEventListener("change", function () {
|
||||||
|
toggleTask(task.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = document.createElement("span");
|
||||||
|
title.className = "task-title";
|
||||||
|
title.textContent = task.title;
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement("button");
|
||||||
|
deleteBtn.className = "task-delete";
|
||||||
|
deleteBtn.textContent = "\u00D7";
|
||||||
|
deleteBtn.title = "Delete task";
|
||||||
|
deleteBtn.addEventListener("click", function () {
|
||||||
|
deleteTask(task.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
li.appendChild(checkbox);
|
||||||
|
li.appendChild(title);
|
||||||
|
li.appendChild(deleteBtn);
|
||||||
|
taskList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
taskForm.addEventListener("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const title = taskInput.value.trim();
|
||||||
|
if (!title) return;
|
||||||
|
|
||||||
|
await fetch("/api/tasks", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ title: title }),
|
||||||
|
});
|
||||||
|
|
||||||
|
taskInput.value = "";
|
||||||
|
fetchTasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function toggleTask(id) {
|
||||||
|
await fetch("/api/tasks/" + id, { method: "PUT" });
|
||||||
|
fetchTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTask(id) {
|
||||||
|
await fetch("/api/tasks/" + id, { method: "DELETE" });
|
||||||
|
fetchTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchTasks();
|
||||||
21
frontend/index.html
Normal file
21
frontend/index.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Task Manager</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Task Manager</h1>
|
||||||
|
<form id="task-form">
|
||||||
|
<input type="text" id="task-input" placeholder="Enter a new task..." required>
|
||||||
|
<button type="submit">Add</button>
|
||||||
|
</form>
|
||||||
|
<ul id="task-list"></ul>
|
||||||
|
<p id="empty-msg" class="empty">No tasks yet. Add one above!</p>
|
||||||
|
</div>
|
||||||
|
<script src="/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
frontend/nginx.conf
Normal file
20
frontend/nginx.conf
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
large_client_header_buffers 4 32k;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://flask:5000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
frontend/style.css
Normal file
120
frontend/style.css
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: #f0f2f5;
|
||||||
|
color: #1a1a2e;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#task-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#task-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#task-input:focus {
|
||||||
|
border-color: #4a6cf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#task-form button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #4a6cf7;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#task-form button:hover {
|
||||||
|
background: #3a5ce5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#task-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item.completed .task-title {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #4a6cf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-delete {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-delete:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
19
prepare-app.sh
Normal file
19
prepare-app.sh
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Preparing app..."
|
||||||
|
|
||||||
|
# Build Docker images
|
||||||
|
echo "Building backend image..."
|
||||||
|
docker build -t taskapp-backend ./backend
|
||||||
|
|
||||||
|
echo "Building frontend image..."
|
||||||
|
docker build -t taskapp-frontend ./frontend
|
||||||
|
|
||||||
|
# Create network (ignore error if it already exists)
|
||||||
|
echo "Creating network..."
|
||||||
|
docker network create taskapp-network 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create named volume (ignore error if it already exists)
|
||||||
|
echo "Creating volume..."
|
||||||
|
docker volume create taskapp-pgdata 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "App prepared successfully."
|
||||||
17
remove-app.sh
Normal file
17
remove-app.sh
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Removing app..."
|
||||||
|
|
||||||
|
# Stop and remove containers
|
||||||
|
docker stop taskapp-nginx taskapp-flask taskapp-db 2>/dev/null
|
||||||
|
docker rm taskapp-nginx taskapp-flask taskapp-db 2>/dev/null
|
||||||
|
|
||||||
|
# Remove images
|
||||||
|
docker rmi taskapp-backend taskapp-frontend 2>/dev/null
|
||||||
|
|
||||||
|
# Remove network
|
||||||
|
docker network rm taskapp-network 2>/dev/null
|
||||||
|
|
||||||
|
# Remove volume
|
||||||
|
docker volume rm taskapp-pgdata 2>/dev/null
|
||||||
|
|
||||||
|
echo "App removed."
|
||||||
39
start-app.sh
Normal file
39
start-app.sh
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Starting app..."
|
||||||
|
|
||||||
|
# Start PostgreSQL
|
||||||
|
echo "Starting database..."
|
||||||
|
docker run -d \
|
||||||
|
--name taskapp-db \
|
||||||
|
--network taskapp-network \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-e POSTGRES_DB=taskapp \
|
||||||
|
-e POSTGRES_USER=taskapp \
|
||||||
|
-e POSTGRES_PASSWORD=taskapp123 \
|
||||||
|
-v taskapp-pgdata:/var/lib/postgresql/data \
|
||||||
|
postgres:15
|
||||||
|
|
||||||
|
# Start Flask backend
|
||||||
|
echo "Starting backend..."
|
||||||
|
docker run -d \
|
||||||
|
--name taskapp-flask \
|
||||||
|
--network taskapp-network \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-e DB_HOST=taskapp-db \
|
||||||
|
-e DB_NAME=taskapp \
|
||||||
|
-e DB_USER=taskapp \
|
||||||
|
-e DB_PASSWORD=taskapp123 \
|
||||||
|
taskapp-backend
|
||||||
|
|
||||||
|
# Start Nginx frontend
|
||||||
|
echo "Starting frontend..."
|
||||||
|
docker run -d \
|
||||||
|
--name taskapp-nginx \
|
||||||
|
--network taskapp-network \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 80:80 \
|
||||||
|
taskapp-frontend
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "App is running!"
|
||||||
|
echo "The app is available at http://localhost:80"
|
||||||
7
stop-app.sh
Normal file
7
stop-app.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Stopping app..."
|
||||||
|
|
||||||
|
docker stop taskapp-nginx taskapp-flask taskapp-db 2>/dev/null
|
||||||
|
docker rm taskapp-nginx taskapp-flask taskapp-db 2>/dev/null
|
||||||
|
|
||||||
|
echo "App stopped."
|
||||||
Loading…
Reference in New Issue
Block a user