A full stack dockerized application with a React (Vite + Bun) frontend, Express backend powered by Bun, and MongoDB database. Deployable locally via Docker Compose and to production on Railway.
This project serves as a complete reference guide for setting up a production-ready full-stack application using Docker containers. It demonstrates best practices for:
- 🐳 Containerizing a React application with Vite
- 🚀 Running an Express.js server in a Docker container
- 🗄️ Integrating MongoDB as a containerized service
- 🌐 Container networking and inter-service communication
- 🔐 Environment variable management across services
- 📦 Docker Compose orchestration
| Layer | Technology |
|---|---|
| Frontend | React + Vite + Bun |
| Backend | Express.js running on Bun |
| Database | MongoDB + Mongoose |
| Containerization | Docker + Docker Compose |
| Deployment | Railway |
fullstack-app-docker/
├── express-container/ # Backend — Express.js + Bun
│ ├── index.js
│ ├── package.json
│ ├── Dockerfile
│ ├── server.js
│ └── .env # local only, gitignored, Backend-specific variables
├── react-container/ # Frontend — React + Vite
│ ├── src/
│ ├── package.json
│ ├── Dockerfile
│ ├── vite.config.js
│ └── .env # local only, gitignored
├── docker-compose.yml # Orchestrates all services
├── .env # Environment variables (root level)
├── .env.example # Template for environment variables
└── .gitignore
MONGO_URI=mongodb://mongo:27017/mydocker
PORT=3000VITE_API_URL=http://localhost:3000
⚠️ Never commit.envfiles. Both are in.gitignore. Use the.env.examplefiles as templates.
- Docker Desktop installed and running
# 1. Clone the repo
git clone https://github.com/masterabdullah95/fullstack-app-docker.git
cd fullstack-app-docker
# 2. Create .env files (see Environment Variables section above)
# 3. Build and start all containers
docker-compose up --buildVite environment variables are only available at build time, not runtime. This requires passing VITE_API_URL during Docker build.
client:
build:
context: ./react-container
dockerfile: Dockerfile
args:
- VITE_API_URL=${VITE_API_URL} # Picks from .env file
ports:
- "${REACT_PORT}:3000"
networks:
- fullstack-netImportant: The .env file must be at the root level (same directory as docker-compose.yml), not in the react-container folder.
... rest of the settings
# Declare build argument
ARG VITE_API_URL
# Make it available to Vite during build
ENV VITE_API_URL=$VITE_API_URL
.... rest of the settings
Key Points:
- Use
ARGto declare build-time variables - Use
ENVto make variables available during build - Use multi-stage builds to minimize image size
- Pass variables via
docker-compose.yml, it will fetch from .env on root, not the Dockerfile directly
In docker-compose.yml:
server:
build:
context: ./express-container
dockerfile: Dockerfile
env_file:
- ./express-container/.env # Load environment variables from .env
environment:
- MONGO_URI=mongodb://mongo:27017/mydocker # Override or add variables
- NODE_ENV=production
ports:
- "${EXPRESS_PORT}:5000"
depends_on:
- mongo
networks:
- fullstack-netKey Points:
- Use
env_fileto load variables from.envduring runtime - Use
environmentto override or add specific variables MONGO_URIshould reference the MongoDB container name (mongo), notlocalhost- Set
depends_onto ensure MongoDB starts before the server
Without a shared network, containers cannot communicate with each other. Services must use container names instead of localhost.
networks:
fullstack-net:
driver: bridgeThen add networks: - fullstack-net to each service.
project-root/
├── .env ✅ CORRECT - at root level
├── docker-compose.yml
├── express-container/
└── react-container/
project-root/
├── react-container/
│ └── .env ❌ WRONG - won't be picked up by docker-compose
In React/Vite (build time):
const apiUrl = import.meta.env.VITE_API_URL;In Express (runtime):
const mongoUri = process.env.MONGO_URI;
const port = process.env.EXPRESS_PORT || 5000;Problem: VITE_API_URL is empty or undefined
Solution:
- Ensure
.envis at root level - Rebuild with
docker-compose up --build - Check build args in
docker-compose.yml
Problem: MongoNetworkError: connect ECONNREFUSED 127.0.0.1:27017
Solution:
- Use
mongodb://mongo:27017/...NOTlocalhost - Use container name, not IP address
- Ensure all services are on the same network
- Check
depends_onin docker-compose
| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend | http://localhost:3000 |
| MongoDB | mongodb://localhost:27017 |
docker-compose down
# To also remove volumes (clears MongoDB data)
docker-compose down -vAll three services run on the same Docker network (fullstack-net) so they can communicate by container name:
react-container → express-container → mongo
(port 5173) (port 3000) (port 27017)
services:
server: # Express backend
client: # React frontend
mongo: # MongoDBContainers communicate internally using container names, e.g.:
- Backend connects to MongoDB via
mongodb://mongo:27017— notlocalhost - Frontend connects to backend via
VITE_API_URL— baked in at build time by Vite
| Method | Endpoint | Description |
|---|---|---|
GET |
/items |
Fetch all items |
POST |
/items |
Add a new item |
curl -X POST http://localhost:3000/items \
-H "Content-Type: application/json" \
-d '{"name": "Test Item"}'curl http://localhost:3000/itemsRailway does not support Docker Compose. Each service is deployed separately using its own Dockerfile.
Railway Project
├── frontend service (react-container/)
├── backend service (express-container/)
└── MongoDB plugin (Railway built-in database)
- Go to Railway → New Project → Deploy from GitHub
- Select
fullstack-app-dockerrepo - In service settings → Root Directory → set to
express-container - Go to Variables tab → add:
MONGO_URI=mongodb://mongo:password@host:port/mydocker?authSource=admin
PORT=3000
- In the same project → Add Service → same GitHub repo
- In service settings → Root Directory → set to
react-container - Go to Variables tab → add:
VITE_API_URL=https://your-backend.up.railway.app
⚠️ Vite bakesVITE_API_URLat build time — set this variable before deploying, and redeploy if you change it.
- In Railway project → Add Service → Database → MongoDB
- Railway creates the database and provides connection variables automatically
- Copy the connection string into your backend service Variables as
MONGO_URI
⚠️ Railway MongoDB requires?authSource=adminat the end of your connection string:mongodb://mongo:password@host:port/mydocker?authSource=admin
Make sure your backend allows requests from your Railway frontend URL (no trailing slash):
app.use(
cors({
origin: [
"http://localhost:5173",
"https://your-frontend.up.railway.app", // no trailing slash
],
credentials: true,
}),
);| Context | Tool | Syntax |
|---|---|---|
| Backend (Node/Bun) | dotenv |
process.env.X |
| Frontend (Vite) | Built into Vite | import.meta.env.VITE_X |
- Backend reads env vars at runtime
- Frontend (Vite) bakes env vars at build time — prefix must be
VITE_ - Never use
dotenvin frontend/browser code
- Containers on the same network communicate by container name
localhostinside a container refers to that container only — not other containers- Always use
mongodb://mongo:27017notmongodb://localhost:27017when connecting from another container
| Local | Railway Production | |
|---|---|---|
| MongoDB | Docker container via Compose | Railway MongoDB plugin |
| Env vars | .env files |
Railway Variables tab |
| How services connect | Docker network | Railway internal networking |
| Frontend build | Vite reads .env |
Vite reads Railway Variables as build args |
Since Vite bakes environment variables at build time, the VITE_API_URL must be passed as a Docker build argument:
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URLIn docker-compose.yml, this is passed from your local .env:
build:
context: ./react-container
args:
- VITE_API_URL=${VITE_API_URL}On Railway, the variable is automatically passed as a build arg from the Variables tab.
MIT