Final Docker multi-service application
This commit is contained in:
commit
99bc49225b
112
README.md
Normal file
112
README.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# Docker Multi-Service Notes Application
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This project is a multi-container web application deployed using Docker. It demonstrates how different services can work together using containerization.
|
||||||
|
|
||||||
|
The application allows users to:
|
||||||
|
|
||||||
|
- Add and delete notes
|
||||||
|
- Upload files
|
||||||
|
- View and download uploaded files
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The application consists of the following services:
|
||||||
|
|
||||||
|
**Frontend (Nginx)**
|
||||||
|
Serves static HTML, CSS, and JavaScript files. Accessible via browser.
|
||||||
|
|
||||||
|
**Backend (Node.js + Express)**
|
||||||
|
Handles API requests and manages notes and file uploads.
|
||||||
|
|
||||||
|
**MongoDB**
|
||||||
|
Stores notes data and uses a persistent Docker volume.
|
||||||
|
|
||||||
|
**Mongo Express**
|
||||||
|
Provides a web interface to view and manage database data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
- Docker and Docker Compose
|
||||||
|
- Node.js with Express
|
||||||
|
- MongoDB (NoSQL database)
|
||||||
|
- Nginx (web server)
|
||||||
|
- HTML, CSS, and JavaScript
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Ports
|
||||||
|
|
||||||
|
| Service | Port |
|
||||||
|
|---------------|------|
|
||||||
|
| Frontend | 8080 |
|
||||||
|
| Backend | 3000 |
|
||||||
|
| Mongo Express | 8081 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Persistent Storage
|
||||||
|
|
||||||
|
The application uses Docker volumes to preserve data across container restarts:
|
||||||
|
|
||||||
|
- `mongo-data` stores database records
|
||||||
|
- `uploads/` stores user-uploaded files
|
||||||
|
|
||||||
|
Data is retained even after stopping the application.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
### 1. Prepare the application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./prepare-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start the application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Once running, open the following in your browser:
|
||||||
|
|
||||||
|
- Application: `http://147.232.204.210:8080`
|
||||||
|
- Database UI: `http://147.232.204.210/:8081`
|
||||||
|
|
||||||
|
### 3. Stop the application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./stop-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Remove the application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./remove-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Application Workflow
|
||||||
|
|
||||||
|
1. The user interacts with the frontend in a browser.
|
||||||
|
2. The frontend sends API requests to the backend.
|
||||||
|
3. The backend processes the requests and communicates with MongoDB.
|
||||||
|
4. MongoDB stores or retrieves the relevant data.
|
||||||
|
5. Mongo Express provides a separate interface to inspect database content directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Create and delete notes
|
||||||
|
- Upload files
|
||||||
|
- View and download uploaded files
|
||||||
|
- Persistent storage via Docker volumes
|
||||||
6
backend/Dockerfile
Normal file
6
backend/Dockerfile
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
FROM node:18
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json .
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
CMD ["node", "app.js"]
|
||||||
58
backend/Dockerfile.save
Normal file
58
backend/Dockerfile.save
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
GNU nano 7.2 backend/Dockerfile
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[ New File ]
|
||||||
|
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location M-U Undo M-A Set Mark M-] To Bracket M-Q Previous ^B Back ^◂ Prev Word
|
||||||
|
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^/ Go To Line M-E Redo M-6 Copy ^Q Where Was M-W Next ^F Forward ^▸ Next Word
|
||||||
57
backend/app.js
Normal file
57
backend/app.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
|
||||||
|
const express = require("express");
|
||||||
|
const mongoose = require("mongoose");
|
||||||
|
const multer = require("multer");
|
||||||
|
const cors = require("cors");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
app.use("/uploads", express.static("uploads"));
|
||||||
|
mongoose.connect("mongodb://mongo:27017/notes");
|
||||||
|
|
||||||
|
const Note = mongoose.model("Note", { text: String });
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: "uploads/",
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
cb(null, Date.now() + "-" + file.originalname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({ storage });
|
||||||
|
|
||||||
|
app.get("/notes", async (req, res) => {
|
||||||
|
res.json(await Note.find());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/notes", async (req, res) => {
|
||||||
|
const note = new Note(req.body);
|
||||||
|
await note.save();
|
||||||
|
res.json(note);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/upload", upload.single("file"), (req, res) => {
|
||||||
|
res.json({ message: "Uploaded" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3000, () => console.log("Backend running"));
|
||||||
|
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.send("Backend is running");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/notes/:id", async (req, res) => {
|
||||||
|
await Note.findByIdAndDelete(req.params.id);
|
||||||
|
res.json({ message: "Deleted" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/files", (req, res) => {
|
||||||
|
fs.readdir("uploads/", (err, files) => {
|
||||||
|
if (err) return res.status(500).send("Error");
|
||||||
|
res.json(files);
|
||||||
|
});
|
||||||
|
});
|
||||||
10
backend/package.json
Normal file
10
backend/package.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "notes-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"mongoose": "^7.0.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"cors": "^2.8.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
38
docker-compose.yml
Normal file
38
docker-compose.yml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
image: nginx
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/usr/share/nginx/html
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo
|
||||||
|
volumes:
|
||||||
|
- mongo-data:/data/db
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
mongo-express:
|
||||||
|
image: mongo-express
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
environment:
|
||||||
|
ME_CONFIG_MONGODB_SERVER: mongo
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
214
frontend/index.html
Normal file
214
frontend/index.html
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Notes Application</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: #f4f6f9;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 500px;
|
||||||
|
margin: 50px auto;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn:hover {
|
||||||
|
background: #1e7e34;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
background: #f1f1f1;
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background: red;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: auto; /* VERY IMPORTANT */
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: darkred;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-link {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.note-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #f1f1f1;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background: red;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Notes Application</h1>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<input id="note" placeholder="Write a note..." />
|
||||||
|
<button onclick="addNote()">Add Note</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul id="list"></ul>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>Upload File</h3>
|
||||||
|
<input type="file" id="file"/>
|
||||||
|
<button class="upload-btn" onclick="uploadFile()">Upload File</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Files</h3>
|
||||||
|
<ul id="files"></ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API = "http://" + location.hostname + ":3000";
|
||||||
|
|
||||||
|
async function loadNotes() {
|
||||||
|
const res = await fetch(API + "/notes");
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
document.getElementById("list").innerHTML =
|
||||||
|
data.map(n => `
|
||||||
|
<li class="note-item">
|
||||||
|
<span>${n.text}</span>
|
||||||
|
<button class="delete-btn" onclick="deleteNote('${n._id}')">Delete</button>
|
||||||
|
</li>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteNote(id) {
|
||||||
|
await fetch(API + "/notes/" + id, { method: "DELETE" });
|
||||||
|
loadNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addNote() {
|
||||||
|
const text = document.getElementById("note").value;
|
||||||
|
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
await fetch(API + "/notes", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({ text })
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("note").value = "";
|
||||||
|
loadNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile() {
|
||||||
|
const fileInput = document.getElementById("file");
|
||||||
|
|
||||||
|
if (!fileInput.files[0]) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", fileInput.files[0]);
|
||||||
|
|
||||||
|
await fetch(API + "/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
alert("File uploaded successfully");
|
||||||
|
loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFiles() {
|
||||||
|
const res = await fetch(API + "/files");
|
||||||
|
const files = await res.json();
|
||||||
|
|
||||||
|
document.getElementById("files").innerHTML =
|
||||||
|
files.map(f => `
|
||||||
|
<li>
|
||||||
|
${f}
|
||||||
|
<a class="file-link" href="http://${location.hostname}:3000/uploads/${f}" target="_blank">Download</a>
|
||||||
|
</li>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNotes();
|
||||||
|
loadFiles();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
prepare-app.sh
Executable file
5
prepare-app.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker network create app-network 2>/dev/null
|
||||||
|
docker volume create mongo-data
|
||||||
|
docker volume create uploads
|
||||||
|
docker-compose build
|
||||||
4
remove-app.sh
Executable file
4
remove-app.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker-compose down -v
|
||||||
|
docker system prune -f
|
||||||
|
|
||||||
5
start-app.sh
Executable file
5
start-app.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo "App running at: http://147.232.204.210:8080"
|
||||||
|
echo "Mongo Express: http://147.232.204.210:8081"
|
||||||
2
stop-app.sh
Executable file
2
stop-app.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker-compose down
|
||||||
BIN
uploads/1774538855945-ast_tree (28).png
Normal file
BIN
uploads/1774538855945-ast_tree (28).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 586 KiB |
BIN
uploads/1774540108005-ast_tree (30).png
Normal file
BIN
uploads/1774540108005-ast_tree (30).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 993 KiB |
BIN
uploads/1774542769899-tseitin_tree (8).jpeg
Normal file
BIN
uploads/1774542769899-tseitin_tree (8).jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
Loading…
Reference in New Issue
Block a user