DEV Community

Cover image for Docker for Absolute Beginners — Part 6
Md Enayetur Rahman
Md Enayetur Rahman

Posted on

Docker for Absolute Beginners — Part 6

Serving a Vite Front-End and Talking to the Outside World

In Part 5 we mastered .env files and interactive flags. Today we flip the direction: instead of the host talking **into* the container, we’ll expose a containerised Vite dev-server to the host (and any browser in your LAN). You’ll see how a React app makes real HTTP calls, how port-binding works, and how to tame Vite + Nodemon inside Docker.*


Learning Aims

By the end of this post you will be able to:

  1. Expose a port from a Docker container to the host ("container → WWW").
  2. Run a modern Vite front-end in Docker with hot-reload.
  3. Globally install tools (nodemon) in the image for flexible dev commands.
  4. Parameterise host-side ports via a .env file.

1 The React App (App.js)

import { useState } from 'react';

export default function App() {
  const [id, setId]   = useState(1);
  const [todo, setTodo] = useState(null);

  const load = async () => {
    const res  = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
    const data = await res.json();
    setTodo(data);
  };

  return (
    <main className="p-6 space-y-4">
      <h1 className="text-2xl font-bold">Todo Fetcher</h1>

      <label className="block">
        <span>Todo ID (1–200): </span>
        <input
          className="border p-1 w-20"
          type="number"
          value={id}
          min="1"
          max="200"
          onChange={(e) => setId(e.target.value)}
        />
      </label>

      <button
        className="px-3 py-1 bg-sky-600 text-white rounded cursor-pointer"
        onClick={load}
      >
        Fetch
      </button>

      {todo && (
        <pre className="bg-gray-100 p-3 rounded">
          {JSON.stringify(todo, null, 2)}
        </pre>
      )}
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

What it does:

  • Keeps two bits of state: the ID to fetch and the todo object returned.
  • load() hits the free JSONPlaceholder API and saves the JSON.
  • Renders an input, a button, and pretty-prints the fetched todo.

🔍 Why Vite? Instant HMR (hot-module reload) and a dev server that listens on 5173 by default.


2 Dockerfile — Word-by-Word

FROM node:20-alpine                    # 1  tiny base image

WORKDIR /usr/src/app                   # 2  set working dir

COPY package*.json ./                  # 3  copy manifests first

RUN npm i -g nodemon \                 # 4a global nodemon
  && npm i                             # 4b project deps

COPY . .                                # 5  rest of source

EXPOSE 5173                             # 6  doc: server listens here

CMD [                                   # 7  default command
  "nodemon", "--legacy-watch",          #   reload-friendly watcher
  "--exec", "vite", "dev", "--", "--host"
]
Enter fullscreen mode Exit fullscreen mode
# Token / segment Beginner-friendly explanation
1 FROM node:20-alpine Start with Node 20 on Alpine Linux — small, quick to download.
2 WORKDIR /usr/src/app All subsequent paths are relative to this folder; if it doesn’t exist Docker creates it.
3 COPY package*.json ./ Grab package.json + lockfile early so the next layer can be cached.
4a npm i -g nodemon This command installs the project dependencies. nodemon is installed globally to automatically restart the server on file changes.
4b npm i Installs app dependencies (React, Vite, etc.).
5 COPY . . Now bring in the app source. Editing code later won’t invalidate the cached dependency layer. This copies all the files and folders from the current directory on the local machine to the working directory inside the container.
6 EXPOSE 5173 Documentation for humans & orchestration tools: the containerised server will listen inside on port 5173. Does NOT publish the port.
7 CMD [...] Default process: 1) nodemon --legacy-watch monitors files (legacy polling works well on bind mounts). 2) --exec vite dev -- --host runs Vite’s dev server and tells it to accept external connections. The --host flag makes the server accessible from outside the container.

3 Compose File — Token-by-Token

HOST_PORT=5173   # in .env (same folder)
Enter fullscreen mode Exit fullscreen mode
version: "3.9"
services:
  web:
    build: .
    container_name: docker-container-to-www-connection

    command: npm run dev -- --host   # we could keep Dockerfile CMD; this overrides for demo

    env_file: .env                   # bulk import HOST_PORT

    ports:
      - "${HOST_PORT}:5173"         # HOST → CONTAINER mapping

    volumes:
      - .:/usr/src/app               # bind mount for live code
      - /usr/src/app/node_modules    # anonymous volume keeps deps isolated

    stdin_open: true                 # -i keep STDIN
    tty: true                        # -t allocate TTY
Enter fullscreen mode Exit fullscreen mode
Key Value What & Why
version 3.9 This specifies the version of the Docker Compose file format we are using.
web This is where we define the different services that make up our application. In this case, we only have one service named web.
build: . This tells Docker Compose to build the Docker image for this service using the Dockerfile located in the current directory.
container_name This sets a custom name for the container created by this service.
command override Replaces Dockerfile CMD at run-time. Here we keep it identical but shows flexibility.
env_file .env This tells Docker Compose to load environment variables from a file named .env in the project directory. This is useful for keeping sensitive information like port numbers out of the docker-compose.yml file.
ports "${HOST_PORT}:5173" Host port (left) comes from .env; container port is 5173 (Vite default). Visiting http://localhost:5173 hits the container.
volumes: - .:/usr/src/app This creates a "bind mount" volume. It maps the current directory on the host machine to the /usr/src/app directory inside the container. This allows for live-reloading of code changes without having to rebuild the image.
volumes: - /usr/src/app/node_modules This creates an "anonymous volume". It prevents the node_modules directory on the host from overwriting the node_modules directory inside the container. This is important because the dependencies might be different between the host and the container.
stdin_open: true This is equivalent to the -i flag in the docker run command. It keeps the standard input open, which is necessary for interactive processes.
tty: true This is equivalent to the -t flag in the docker run command. It allocates a pseudo-TTY, which is also necessary for interactive processes.

4 Running and Reaching the Container

To run and test the project, you need to have Docker and Docker Compose installed on your machine.

docker-compose up -d --build

This command is used to start the application.

Part Description
docker-compose The command-line tool for Docker Compose.
up This subcommand tells Docker Compose to start the services defined in the docker-compose.yml file.
-d This flag stands for "detached mode". It runs the containers in the background.
--build This flag tells Docker Compose to build the Docker image before starting the containers. This is useful when you have made changes to the Dockerfile.
  1. Open http://localhost:5173 in your browser.
  2. Type an ID (1-200) ➜ click Fetch.
  3. The containerised frontend makes an outbound request to jsonplaceholder.typicode.com → data displayed.

🎉 Container ➜ Host Browser ➜ Public API — full outside-world flow.

LAN access: on the same Wi-Fi, another device can hit http://<your-laptop-IP>:5173 because port 5173 is published on the host interface.

docker-compose down

This command is used to stop the application.

Part Description
docker-compose The command-line tool for Docker Compose.
down This subcommand stops and removes the containers, networks, and volumes created by docker-compose up.

5 What if .env is Missing or Port is In-Use?

  • No .env${HOST_PORT} expands to blank, causing a Compose error: “invalid published port”.
  • Port already bound on host ➜ Compose fails: “driver failed programming external connectivity”.
    • Fix: change HOST_PORT in .env (e.g., 3001) and restart.

6 Conclusion

✅ Exposed a containerised Vite dev server to the host & LAN.

✅ Used ports: HOST:CONTAINER with variable substitution from .env.

✅ Learned why --host is required for Vite inside Docker.

✅ Installed nodemon globally and leveraged it in CMD for reload-on-save.

Happy containerised front-end hacking! 🚀

Top comments (0)