Ravindra BagaleCourses & study guides मराठी

Chapter 57: Docker Projects Part 1: Websites, Databases, LAMP, LEMP, WordPress, 3-Tier and Every Language with MySQL and MongoDB

57.1 Project 1: Launch a website with a container (nginx + pandoc build)

Why: the problem

Suppose we work at a small travel startup in Pune, like a mini MakeMyTrip. The marketing team wants a simple company website live today. The old way is to log in to an EC2 server, install nginx by hand, copy files with scp and edit configs on the server. Next month a new developer asks: which nginx version is it? Which config? Nobody knows. If the server dies, we must repeat everything from memory. With Docker, the website and the web server become one image. The same image runs on a laptop, on EC2 and on any cloud. To update the site we build a new image with a new tag. To roll back we run the old tag again, in seconds. In this project we write the pages in Markdown, convert them to HTML inside a build stage, and serve them with nginx. We also add our own nginx config, a health check, gzip, caching headers and a custom 404 page. This is the first real project, so we go slowly and check the output of every command.

What: every term in plain English

  • Static website — Pages that are plain files: HTML, CSS and images. No database and no server code.
  • nginx — A fast web server. It reads files from a folder and sends them to the browser.
  • pandoc — A tool that converts Markdown text into HTML pages. We run it inside a build stage.
  • Multi-stage build — A Dockerfile with two FROM lines. Stage 1 builds the pages, stage 2 only serves them.
  • Image tag — A version label on an image, like site:1.0 and site:1.1. We roll back by running an old tag.
  • Port mapping — -p 8080:80 means: traffic to port 8080 on our machine goes to port 80 in the container.
  • HEALTHCHECK — A command Docker runs every few seconds to see if the site still answers.
  • Document root — The folder nginx serves. In the nginx image it is /usr/share/nginx/html.
Write the pages in Markdown. Write the nginx config. Write a two-stage Dockerfile. Build the image with a tag. Run the container. Test it. Update, roll back and clean up.
Markdown to HTML to nginx container to browser

How: step by step

  1. Write the pages in Markdown. index.md, about.md and 404.md in a content folder.
  2. Write the nginx config. site.conf with gzip, cache headers and the 404 page.
  3. Write a two-stage Dockerfile. pandoc converts Markdown to HTML; nginx serves it.
  4. Build the image with a tag. docker build -t travelsite:1.0 .
  5. Run the container. docker run -d -p 8080:80 --name web travelsite:1.0
  6. Test it. curl, the browser and docker inspect for health.
  7. Update, roll back and clean up. Build 1.1, switch, switch back, then remove everything.

Architecture

Browser: http://server:8080 Host port 8080 (security group allows it) Container web: nginx 1.27-alpine /usr/share/nginx/html index.html about.html 404.html Built from Markdown by pandoc (build stage, not shipped)
Website container architecture

Folder layout

travelsite/
  content/
    index.md
    about.md
    404.md
  site.conf
  Dockerfile
  .dockerignore
  compose.yaml

Example 1: Build the pages and the image (version 1

Goal. Build the pages and the image (version 1.0).

content/index.md

---
title: Sahyadri Trips
---

# Welcome to Sahyadri Trips

Weekend treks from Pune and Mumbai.

## Popular trips

- Rajgad fort trek, every Saturday
- Harihar fort, monsoon special
- Kalsubai peak sunrise trek

## Contact

Call us on +91 00000 00000 or write to hello@example.com.

content/about.md

---
title: About us
---

# About Sahyadri Trips

We are a small team of trek leaders.
We started in 2019 with one bus and twenty trekkers.

content/404.md

---
title: Page not found
---

# Oops, this page is not here

Go back to the [home page](/).

site.conf

server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    gzip on;
    gzip_types text/css application/javascript image/svg+xml;

    location / {
        try_files $uri $uri.html $uri/ =404;
    }

    location ~* \.(css|png|jpg|svg)$ {
        expires 7d;
        add_header Cache-Control "public";
    }

    location = /healthz {
        access_log off;
        return 200 "ok";
    }

    error_page 404 /404.html;
}

Dockerfile

# Stage 1: convert Markdown pages to HTML
FROM pandoc/core:3.5 AS build
WORKDIR /src
COPY content/ ./content/
RUN mkdir -p /out && for f in content/*.md; do \
      name=$(basename "$f" .md); \
      pandoc "$f" --standalone --output "/out/$name.html"; \
    done

# Stage 2: serve the HTML with nginx
FROM nginx:1.27-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY site.conf /etc/nginx/conf.d/site.conf
COPY --from=build /out/ /usr/share/nginx/html/
EXPOSE 80
HEALTHCHECK --interval=15s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost/healthz || exit 1

.dockerignore

.git
*.log
notes/

Commands

docker build -t travelsite:1.0 .
docker images travelsite

What each line does

  • FROM pandoc/core:3.5 AS build — The build stage. It has pandoc installed and nothing else.
  • for f in content/*.md — Loops over every Markdown page.
  • pandoc ... --standalone — Makes a full HTML page with head and body from Markdown.
  • FROM nginx:1.27-alpine — The final stage. A small nginx image, about 20 MB.
  • rm default.conf — Removes the sample config so only our config is used.
  • COPY --from=build — Takes only the HTML files from stage 1. pandoc does not ship.
  • try_files $uri $uri.html — Lets /about open about.html without typing .html.
  • location = /healthz — A tiny URL used only by the health check.
  • HEALTHCHECK ... wget — Docker marks the container healthy or unhealthy.

Green output

[+] Building 9.8s (12/12) FINISHED
 => [build 4/4] RUN mkdir -p /out && for f in content/*.md; do ...
 => [stage-1 3/4] COPY site.conf /etc/nginx/conf.d/site.conf
 => [stage-1 4/4] COPY --from=build /out/ /usr/share/nginx/html/
 => naming to docker.io/library/travelsite:1.0

REPOSITORY   TAG   IMAGE ID       CREATED          SIZE
travelsite   1.0   5c1d9e2a7b31   10 seconds ago   48.2MB

Expected result. We have one small image. It holds three HTML pages and our nginx config. pandoc is not inside it, so the image stays small.

Example 2: Run the website and test it like a user and like an admin

Goal. Run the website and test it like a user and like an admin.

Commands

docker run -d --name web -p 8080:80 --restart unless-stopped travelsite:1.0
docker ps --filter name=web
curl -s http://localhost:8080/ | grep -o "Welcome to Sahyadri Trips"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/about
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/missing-page
curl -sI -H "Accept-Encoding: gzip" http://localhost:8080/ | grep -i content-encoding
docker inspect -f '{{.State.Health.Status}}' web
docker logs --tail 5 web

What each line does

  • --restart unless-stopped — Starts the site again after a server reboot.
  • grep -o — Prints only the matching text, so we know the page is right.
  • -w %{http_code} — Prints only the HTTP status code.
  • /missing-page — Should give 404 and our own 404 page.
  • Accept-Encoding: gzip — Checks that gzip is on.
  • State.Health.Status — starting, then healthy after the first check passes.

Green output

CONTAINER ID   IMAGE            STATUS                    PORTS                  NAMES
a91f0c2d3e4b   travelsite:1.0   Up 20 seconds (healthy)   0.0.0.0:8080->80/tcp   web
Welcome to Sahyadri Trips
200
404
Content-Encoding: gzip
healthy
172.17.0.1 - - [25/Oct/2026:10:12:01 +0000] "GET /about HTTP/1.1" 200 812 "-" "curl/8.5.0"

Expected result. Open http://localhost:8080 in the browser (or http://EC2-PUBLIC-IP:8080 after opening the port in the security group). The home page, the about page and the 404 page all work.

Example 3: Ship version 1

Goal. Ship version 1.1, then roll back to 1.0 in seconds.

content/index.md (one new line)

## New this month

- Diwali special: Raigad fort night trek

Commands

docker build -t travelsite:1.1 .
docker rm -f web
docker run -d --name web -p 8080:80 --restart unless-stopped travelsite:1.1
curl -s http://localhost:8080/ | grep -o "Diwali special"

# something is wrong with 1.1? roll back:
docker rm -f web
docker run -d --name web -p 8080:80 --restart unless-stopped travelsite:1.0
curl -s http://localhost:8080/ | grep -c "Diwali special"

What each line does

  • build -t travelsite:1.1 — A new image. The old 1.0 image is still on disk.
  • rm -f web — Stops and removes the old container. The image stays.
  • run ... :1.1 — Starts the new version on the same port.
  • run ... :1.0 — Rollback is just running the old tag again.
  • grep -c — Counts matches. 0 means the new line is gone, so rollback worked.

Green output

Diwali special
...
0

Expected result. Update and rollback each take a few seconds. There is no editing on the server, and every version is an image we can run again.

Example 4: Run the same site with Docker Compose

Goal. Run the same site with Docker Compose.

compose.yaml

services:
  web:
    build: .
    image: travelsite:1.1
    ports:
      - "8080:80"
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /var/cache/nginx
      - /var/run

Commands

docker compose up -d --build
docker compose ps
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/
docker compose logs --tail 3 web

What each line does

  • build: . — Compose builds the Dockerfile in this folder.
  • image: travelsite:1.1 — The tag Compose gives the image it builds.
  • read_only: true — The container cannot change its own files. Safer.
  • tmpfs — nginx still needs two writable folders. tmpfs gives them in memory.
  • up -d --build — Builds and starts in the background.

Green output

NAME            IMAGE            SERVICE   STATUS                    PORTS
travel-web-1    travelsite:1.1   web       Up 6 seconds (healthy)    0.0.0.0:8080->80/tcp
200

Expected result. One command starts the whole site. Next projects add a database next to this web service.

Every command, step by step (plain docker and Compose)

  1. Make the folder and files

    mkdir -p travelsite/content && cd travelsite
    ls
    

    What it does: Creates the project folder. We then create index.md, about.md, 404.md, site.conf and Dockerfile with an editor like nano or VS Code.

    content
    
  2. Check the Docker engine

    docker version --format '{{.Server.Version}}'
    

    What it does: Makes sure Docker is running before we build.

    27.3.1
    
  3. Build version 1.0

    docker build -t travelsite:1.0 .
    

    What it does: Runs both stages and tags the final image.

    naming to docker.io/library/travelsite:1.0
    
  4. See the layers and size

    docker history travelsite:1.0 --format '{{.Size}}  {{.CreatedBy}}' | head -5
    

    What it does: Shows our COPY layers are only a few KB.

    8.19kB   COPY /out/ /usr/share/nginx/html/ # buildkit
    4.10kB   COPY site.conf /etc/nginx/conf.d/site.conf
    
  5. Look inside the image

    docker run --rm travelsite:1.0 ls /usr/share/nginx/html
    

    What it does: Lists the HTML files that pandoc made.

    404.html
    about.html
    index.html
    
  6. Test the nginx config

    docker run --rm travelsite:1.0 nginx -t
    

    What it does: nginx checks the config syntax.

    nginx: configuration file /etc/nginx/nginx.conf test is successful
    
  7. Run the site

    docker run -d --name web -p 8080:80 travelsite:1.0
    

    What it does: Starts the container in the background.

    a91f0c2d3e4b5f...
    
  8. Wait for healthy

    docker ps --filter name=web --format '{{.Status}}'
    

    What it does: After about 15 seconds the status shows healthy.

    Up 18 seconds (healthy)
    
  9. Test from the server

    curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/
    

    What it does: 200 means the site is up.

    200
    
  10. Follow the access log

    docker logs -f web
    

    What it does: Open the site in the browser and watch requests arrive. Press Ctrl+C to stop following.

    GET / HTTP/1.1" 200
    
  11. Open the port on EC2 (if on AWS)

    aws ec2 authorize-security-group-ingress --group-id sg-0123456789abcdef0 --protocol tcp --port 8080 --cidr 0.0.0.0/0
    

    What it does: Lets the internet reach port 8080. On a laptop skip this step.

    "Return": true
    
  12. Clean up

    docker rm -f web
    docker compose down
    docker image rm travelsite:1.0 travelsite:1.1
    docker image prune -f
    

    What it does: Removes containers, images and dangling build layers.

    Untagged: travelsite:1.0
    Untagged: travelsite:1.1
    Total reclaimed space: 48.2MB
    

When it is red 1

docker: Error response from daemon: driver failed programming external connectivity on endpoint web: Bind for 0.0.0.0:8080 failed: port is already allocated.

Cause. Another container or program already uses port 8080.

Fix, step by step

  1. Run docker ps and look for 0.0.0.0:8080 in the PORTS column.
  2. Stop that container with docker rm -f NAME, or use another port like -p 8081:80.
  3. On Linux check other programs with sudo ss -ltnp | grep 8080.

When it is red 2

nginx: [emerg] unknown directive "gzip_typess" in /etc/nginx/conf.d/site.conf:7

Cause. A typing mistake in site.conf. The container starts and exits at once.

Fix, step by step

  1. Run docker logs web to see this message.
  2. Fix the word in site.conf.
  3. Test before running: docker run --rm travelsite:1.0 nginx -t.
  4. Build again and run again.

When it is red 3

curl: (7) Failed to connect to 13.233.10.20 port 8080 after 21003 ms: Couldn't connect to server

Cause. Works on the server with localhost but not from the laptop. The EC2 security group blocks the port.

Fix, step by step

  1. Check curl localhost:8080 on the server first. If it works, Docker is fine.
  2. Add an inbound rule for TCP 8080 in the security group.
  3. Check that the instance has a public IP and you use http, not https.

When it is red 4

pandoc: content/index.md: openBinaryFile: does not exist (No such file or directory)

Cause. The content folder was not copied, often because .dockerignore lists it or the folder name is wrong.

Fix, step by step

  1. Run ls content in the project folder.
  2. Check .dockerignore does not contain content.
  3. Build again with docker build --no-cache -t travelsite:1.0 .

Practice tasks

Easy Task 1: Build and run

Why: Start.

What to do: Build travelsite:1.0 and open it on port 8080.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
docker build -t travelsite:1.0 .
docker run -d --name web -p 8080:80 travelsite:1.0
curl -s localhost:8080 | grep Welcome

Easy Task 2: Second port

Why: Two copies.

What to do: Run a second copy of the same image on port 8081.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
docker run -d --name web2 -p 8081:80 travelsite:1.0
curl -s -o /dev/null -w '%{http_code}' localhost:8081

Easy Task 3: Look inside

Why: Know the image.

What to do: List the files nginx serves.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
docker exec web ls -l /usr/share/nginx/html

Medium Task 4: Contact page

Why: Grow the site.

What to do: Add contact.md, build 1.2 and open /contact.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
docker build -t travelsite:1.2 .
docker rm -f web
docker run -d --name web -p 8080:80 travelsite:1.2
curl -s localhost:8080/contact | grep -i contact

Medium Task 5: Rollback

Why: Safety.

What to do: Roll back from 1.2 to 1.1 and prove it.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
docker rm -f web
docker run -d --name web -p 8080:80 travelsite:1.1
curl -s -o /dev/null -w '%{http_code}' localhost:8080/contact

Medium Task 6: Health

Why: Monitoring.

What to do: Break the health check on purpose and watch it turn unhealthy.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
docker exec web rm /etc/nginx/conf.d/site.conf
docker exec web nginx -s reload
sleep 50
docker inspect -f '{{.State.Health.Status}}' web

Medium Task 7: Cache header

Why: Speed.

What to do: Add style.css and check the Expires header.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
curl -sI localhost:8080/style.css | grep -i -E 'expires|cache-control'

Hard Task 8: Read-only

Why: Security.

What to do: Run with --read-only and the two tmpfs folders.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
docker run -d --name ro -p 8082:80 --read-only --tmpfs /var/cache/nginx --tmpfs /var/run travelsite:1.1

Hard Task 9: Size check

Why: Lean images.

What to do: Compare the size of pandoc/core and travelsite.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E 'pandoc|travelsite'

Easy Task 10: Clean up

Why: Habit.

What to do: Remove all containers and images from this project.

How:
  1. Use any Ubuntu 22.04/24.04 VM or a laptop with Docker Engine or Docker Desktop.
  2. Make a new empty folder for the project.
  3. Type every file yourself, then compare with the solution.
Solution
docker rm -f web web2 ro
docker image rm travelsite:1.0 travelsite:1.1 travelsite:1.2

Homework: try at home

Add a contact page (contact.md) and a CSS file served with the 7-day cache header. Build 1.2, check the header with curl -I, then roll back to 1.1.

Recap

  • A static website becomes one nginx image.
  • Build the pages in one stage and serve them in another.
  • Test the config with nginx -t before running.
  • Each release is a tag; rollback is running the old tag.
  • Open the port in the EC2 security group for outside access.
  • Clean up containers and images after the project.

FAQ

Q. Do I need to know HTML?

No. We write Markdown and pandoc makes the HTML. Later you can add your own HTML and CSS files the same way.

Q. Can I use port 80 instead of 8080?

Yes: -p 80:80. On Linux port 80 is free only if no other web server runs on the host.

Q. Where do the files live after I remove the container?

Inside the image. The container is only a running copy, so removing it loses nothing.

Q. How do I add a domain name?

Point an A record (Route 53 or GoDaddy) to the server IP, and use port 80. Project 6 adds HTTPS.

Q. Why alpine?

The nginx alpine image is small and starts fast. It is fine for static sites.

Q. Should I edit files inside the running container?

No. Changes are lost when the container is replaced. Change the source, build a new tag, run it.

Interview questions

  1. How would you host a static website with Docker?

    Model answer: Multi-stage build: build the pages in stage 1, copy only the output into an nginx image, add a config and health check, and run with -p 80:80 and a restart policy.

  2. How do you roll back a containerised website?

    Model answer: Keep versioned tags and run the previous tag again; never use only latest.

  3. What is the difference between EXPOSE and -p?

    Model answer: EXPOSE is documentation inside the image; -p actually publishes the port on the host.

  4. How do you check a container is healthy?

    Model answer: A HEALTHCHECK in the Dockerfile, then docker ps or docker inspect -f '{{.State.Health.Status}}'.

  5. Why make the container read-only?

    Model answer: An attacker cannot change site files or drop tools; tmpfs gives only the folders nginx must write.

Quick quiz

Q1. Why do we use two stages in this Dockerfile?

Stage 1 has pandoc to build the pages. Stage 2 only has nginx and the HTML, so the final image is small and has fewer tools for attackers.

Q2. How do you roll back the website?

Remove the running container and run the older image tag again, for example travelsite:1.0.

Q3. What does -p 8080:80 mean?

Port 8080 on the host forwards to port 80 inside the container.

Q4. How do you check the nginx config inside the image?

docker run --rm travelsite:1.0 nginx -t

Q5. Why does the site work on localhost but not from the internet on EC2?

The security group does not allow the port. Add an inbound rule.

Samajla ka?

Ravi Sir's Cards for this lesson

मराठी cards