Simple Nginx proxy pass with frontend and backend
Diagram
In my scenario, I am using the traditional method to deploy the Vue application. I set up an Nginx server in a container and map the build files of the Vue app to the Nginx HTML volume. The server is configured to proxy requests to a specific path, /form
. Additionally, I will deploy FastAPI in a separate container, which will also use proxying for requests directed to the path /api
.
Nginx
Make certs folder and generate ssl certificates first.
1
2
mkdir certs
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./certs/example.key -out ./certs/example.crt
Create nginx.conf
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
upstream fastapi {
server 192.168.50.245:8000
}
server {
listen 80;
server_name test.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name test.example.com;
ssl_certificate /etc/nginx/ssl/example.crt;
ssl_certificate_key /etc/nginx/ssl/example.key;
location / {
root /var/www/html;
index index.html index.htm;
}
location /api {
proxy_pass http://fastapi;
}
}
Create docker-compose.yml
1
2
3
4
5
6
7
8
9
10
services:
nginx:
image: nginx:latest
ports:
- 80:80
- 443:443
volumes:
- ./html:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./certs:/etc/nginx/ssl
Create a html for the main page. mkdir html && vim html/index.html
1
<h1>Main Page</h1>
FastAPI
Install fastapi and uvicorn pip install fastapi uvicorn
Create main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from fastapi import FastAPI, APIRouter
from pydantic import BaseModel
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
# root_path use for behind the proxy
app = FastAPI(root_path="/api")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Change this to specific origins in production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Define a Pydantic model for the incoming data
class UserEntry(BaseModel):
fname: str
lname: str
age: int
@app.post("/user")
async def create_entry(user_entry: UserEntry):
user_data = user_entry.dict()
# Here you can process the data (e.g., save it to a database)
# For now, we will just return the received data as a response
return JSONResponse(content={"message": "User entry received", "data": user_data})
Then export your requirements.txt
, pip freeze >> requirements.txt
Prepare a Dockerfile
for running in docker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM python:3.10
# Set the working directory in the container
WORKDIR /app
# Copy the code & requirements file into the container
COPY ./requirements.txt .
COPY ./main.py .
# Install the dependencies specified in requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
# Command to run the FastAPI application using Uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Create a docker-compose.yml
for easy to deploy.
1
2
3
4
5
6
7
services:
fastapi:
container_name: fastapi
build: .
ports:
- 8000:8000
restart: unless-stopped
Vue app
Initial vue app by vite
npm create vite@latest my-vue-app -- --template vue
package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "vue-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.7",
"vue": "^3.5.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.10"
}
}
Install node modules with using npm i
In vite.config.js
, add a base url section is needed, we can import from a .env.production file as well.
1
2
3
4
5
6
7
8
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vite.dev/config/
export default defineConfig({
base: "https://test.example.com/form",
plugins: [vue()]
});
Edit App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import UserForm from "./components/UserForm.vue";
</script>
<template>
<UserForm />
</template>
<style>
body {
font-family: Arial, sans-serif;
background-color: #fff;
margin: 0;
padding: 20px;
}
</style>
Create ./components/UserForm.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<template>
<div>
<h3>User Entry Form</h3>
<form @submit.prevent="submitEntry">
<div>
<label for="fname">First Name:</label>
<input type="text" v-model="form.fname" id="fname" required />
</div>
<div>
<label for="lname">Last Name:</label>
<input type="text" v-model="form.lname" id="lname" required />
</div>
<div>
<label for="age">Age:</label>
<input type="number" v-model="form.age" id="age" required />
</div>
<button type="submit">Submit</button>
</form>
<p v-if="loading">Submitting...</p>
<p v-if="user_response"></p>
</div>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
form: {
fname: "",
lname: "",
age: ""
},
loading: false,
user_response: null
};
},
methods: {
async submitEntry() {
this.loading = true;
try {
const res = await axios.post(
"https://test.example.com/api/user",
this.form
);
this.user_response = res.data; // Handle the response from the server
} catch (error) {
console.error(error);
this.user_response = "Error submitting data";
} finally {
this.loading = false;
}
}
}
};
</script>
<style scoped>
h3 {
color: #000;
text-align: center;
}
form {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 400px;
margin: auto;
padding: 20px;
}
div {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #000;
font-weight: bold;
text-align: left;
}
input[type="text"],
input[type="number"] {
width: calc(100% - 10px);
padding: 8px;
border-radius: 4px;
border: 1px solid #ccc;
}
input[type="text"]:focus,
input[type="number"]:focus {
border-color: #007bff; /* Change this color as needed */
button {
background-color: #007bff; /* Primary button color */
color: white;
border: none;
border-radius: 4px;
padding: 10px;
cursor: pointer;
width: 100%;
}
button:hover {
background-color: #0056b3; /* Darker shade on hover */
}
p {
color: #000;
text-align: center;
}
</style>
Build the vue app with using npm run build
, you will get the dist
build files
1
2
3
4
5
6
7
8
9
10
.
├── dist
├── index.html
├── node_modules
├── package.json
├── package-lock.json
├── public
├── README.md
├── src
└── vite.config.js
Then we copy the context of dist to map nginx HTML volume,
cp -r vue-app/dist/* nginx/html/form/.
Docker Compose
1
docker-compose up -d -f nginx/docker-compose.yml -f fastapi/docker-compose.yml
DNS record
Add a hostname resolve for accessing the domain in locally.
echo "192.168.50.245 test.example/com" >> /etc/hosts
Now you can go to https://test.example.com/form
on the browser.
Conclusion
For CICD, I recommend to deploy a container with vue app as well. Then compose all the services in docker-compose.yml
I create a Dockerfile for vue app:
1
2
3
4
5
6
7
8
9
10
11
12
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
docker-compose.yml
1
2
3
4
5
6
7
services:
vue-app:
container_name: vue-app
build: .
ports:
- 3000:80
restart: unless-stopped
In nginx proxy configuration:
1
2
3
4
5
6
7
8
upstream vue-app {
server 192.168.50.245:3000
}
server {
location /form {
proxy_pass http://vue-app/;
}
}