Deploying a TODO Application Using Docker (with Docker Volumes)

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:

  1. Frontend: The part users see and interact with (web page)
  2. Backend: The server that handles requests and talks to the database
  3. 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.sql

Backend 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 80

Quick 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 ls

Think 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 ls

This 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.0

Key Points:

  • -v todo-mysql-data:/var/lib/mysql connects our volume to MySQL’s data folder
  • --network todo-network adds this container to our private network
  • The -e flags 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 ./backend

Run Backend Container:

# Run the backend connected to our network
docker run -d \
  –name todo-backend \
  –network todo-network \
  -p 3000:3000 \
  todo-backend

The 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 ./frontend

Run Frontend Container:

# Run the frontend
docker run -d \
  –name todo-frontend \
  –network todo-network \
  -p 8080:80 \
  todo-frontend

Your 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-network

Database 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.0

Backend Commands:

# Build and run backend
docker build -t todo-backend ./backend
docker run -d –name todo-backend –network todo-network \
  -p 3000:3000 todo-backend

Frontend Commands:

# Build and run frontend
docker build -t todo-frontend ./frontend
docker run -d –name todo-frontend –network todo-network \
  -p 8080:80 todo-frontend

Check 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/todos

Conclusion

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.