A beginner-friendly guide to running a full-stack TODO app with Docker. Learn how Docker Volumes protect your data even when containers are deleted, all without using Docker Compose.
Introduction
I recently worked on a DevOps task to run a TODO application using Docker. The goal was to use Docker Volumes to keep data safe, without using Docker Compose. This experience helped me understand how Docker really works.
In this guide, I’ll walk you through each step. You’ll learn how to run a simple TODO app with three parts: a frontend, a backend, and a database. Most importantly, you’ll see how Docker Volumes keep your data safe when containers get deleted.
Understanding the Parts of Our App
Our TODO application has three main pieces that work together:
- Frontend: The part users see and interact with (web page)
- Backend: The server that handles requests and talks to the database
- Database: Where all the TODO items are stored
Each piece runs in its own Docker container. They need to talk to each other, and the database data needs to stay safe even if we delete the container.
Why Docker Volumes Matter
Normally, when you delete a Docker container, all the data inside it disappears too. Docker Volumes solve this problem by storing data separately from the container. This is perfect for databases because you don’t want to lose your data every time you update your app.
Let’s Build It Step by Step
First, create a folder structure for your project. Here’s what it looks like:
Project Folder Structure:
todo-app/
├── frontend/
│ ├── Dockerfile
│ ├── index.html
│ └── style.css
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ └── server.js
└── database/
└── init.sqlBackend Server Code (server.js):
const express = require(‘express’);
const mysql = require(‘mysql2’);
// Create Express app
const app = express();
// Connect to MySQL database
const db = mysql.createConnection({
host: ‘mysql-db’, // This name comes from our Docker container
user: ‘todo_user’,
password: ‘password123’,
database: ‘todo_db’
});
// Simple API to get all TODOs
app.get(‘/todos’, (req, res) => {
db.query(‘SELECT * FROM todos’, (err, results) => {
if (err) {
res.status(500).json({ error: err.message });
} else {
res.json(results);
}
});
});
// Start the server on port 3000
app.listen(3000, () => {
console.log(‘Backend server is running on port 3000’);
});Create Dockerfiles for Each Part
Backend Dockerfile (backend/Dockerfile):
# Start with Node.js
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy and install dependencies
COPY package*.json ./
RUN npm install
# Copy the rest of the code
COPY . .
# Tell Docker this container uses port 3000
EXPOSE 3000
# Command to start the app
CMD [“node”, “server.js”]Frontend Dockerfile (frontend/Dockerfile):
# Start with Nginx (a web server)
FROM nginx:alpine
# Copy website files to Nginx folder
COPY . /usr/share/nginx/html
# Tell Docker this container uses port 80
EXPOSE 80Quick Tip: You don’t always need a custom Dockerfile for the database. We’ll use the official MySQL image directly, which is simpler.
Create a Docker Volume for Your Data
This is the most important step for keeping your data safe
Create a Docker Volume:
# Create a volume named “todo-mysql-data”
docker volume create todo-mysql-data
# Check that it was created
docker volume lsThink of a Docker Volume like an external hard drive for your container. Even if you delete the container, the data on the volume stays safe.
Create a Network for Your Containers
Containers need a way to talk to each other. We’ll create a private network:
Create a Docker Network:
# Create a network named “todo-network”
docker network create todo-network
# Check your networks
docker network lsThis network is like a private chat room where only your containers can talk to each other.
Run the Database Container
Now let’s start the MySQL database with the volume we created:
Start MySQL with Volume:
# Run MySQL container with our volume and network
docker run -d \
–name mysql-db \
–network todo-network \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_DATABASE=todo_db \
-e MYSQL_USER=todo_user \
-e MYSQL_PASSWORD=password123 \
-p 3306:3306 \
mysql:8.0Key Points:
-v todo-mysql-data:/var/lib/mysqlconnects our volume to MySQL’s data folder--network todo-networkadds this container to our private network- The
-eflags set up the database name, username, and password
Build and Run the Backend
Now let’s build and run the backend server:
Build Backend Image:
# Go to backend folder and build the image
docker build -t todo-backend ./backendRun Backend Container:
# Run the backend connected to our network
docker run -d \
–name todo-backend \
–network todo-network \
-p 3000:3000 \
todo-backendThe backend can now talk to the database using the name mysql-db because they’re on the same network.
Build and Run the Frontend
Finally, let’s start the frontend:
Build Frontend Image:
# Go to frontend folder and build the image
docker build -t todo-frontend ./frontendRun Frontend Container:
# Run the frontend
docker run -d \
–name todo-frontend \
–network todo-network \
-p 8080:80 \
todo-frontendYour TODO app is now running! Open your browser and go to http://localhost:8080 to see it.
Test That Your Data is Safe
Let’s prove that Docker Volumes really work:
Test Data Safety:
# First, add some TODOs through your app
# Then, stop and remove the database container
docker stop mysql-db
docker rm mysql-db
# Create a new MySQL container with the SAME volume
docker run -d \
–name new-mysql-db \
–network todo-network \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_DATABASE=todo_db \
-e MYSQL_USER=todo_user \
-e MYSQL_PASSWORD=password123 \
-p 3307:3306 \
mysql:8.0
# Your TODOs should still be there when you check the app!
This test shows that even though we deleted the database container, all our TODO items were saved in the Docker Volume.
All Commands in One Place
Here’s a quick reference of all the commands you need:
Setup Commands:
# Create volume and network
docker volume create todo-mysql-data
docker network create todo-networkDatabase Commands:
# Run MySQL with volume
docker run -d –name mysql-db –network todo-network \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_DATABASE=todo_db \
-e MYSQL_USER=todo_user \
-e MYSQL_PASSWORD=password123 \
-p 3306:3306 mysql:8.0Backend Commands:
# Build and run backend
docker build -t todo-backend ./backend
docker run -d –name todo-backend –network todo-network \
-p 3000:3000 todo-backendFrontend Commands:
# Build and run frontend
docker build -t todo-frontend ./frontend
docker run -d –name todo-frontend –network todo-network \
-p 8080:80 todo-frontendCheck Everything is Working:
# See all running containers
docker ps
# Check the backend logs
docker logs todo-backend
# Test the backend API
curl http://localhost:3000/todosConclusion
This project taught me that Docker is more than just a tool to run applications. In fact, it’s a complete system for building, shipping, and running apps reliably. Docker Volumes are a key part of this system, especially for apps that need to save data.
The best part was seeing my TODO items still there after deleting the database container. Indeed, that’s the true power of Docker Volumes!
Now that I understand these basics, I’m ready to learn more advanced tools. Specifically, Docker Compose and Kubernetes build on these same concepts. In summary, this exercise gave me a solid start on my DevOps journey.



