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:
- Expose a port from a Docker container to the host ("container → WWW").
- Run a modern Vite front-end in Docker with hot-reload.
-
Globally install tools (
nodemon
) in the image for flexible dev commands. -
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>
);
}
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"
]
# | 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)
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
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 . |
- Open http://localhost:5173 in your browser.
- Type an ID (1-200) ➜ click Fetch.
- 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.
- Fix: change
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)