diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index e012998..3e92d0c 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -44,11 +44,21 @@ services:
networks:
- control
+ public-web:
+ build:
+ context: ..
+ dockerfile: public-web/Dockerfile
+ ports:
+ - "8082:80"
+ networks:
+ - control
+
reverse-proxy:
image: nginx:1.27-alpine
depends_on:
- backend
- admin-web
+ - public-web
ports:
- "80:80"
volumes:
diff --git a/deploy/nginx/reverse-proxy.conf b/deploy/nginx/reverse-proxy.conf
index 70a7216..c960149 100644
--- a/deploy/nginx/reverse-proxy.conf
+++ b/deploy/nginx/reverse-proxy.conf
@@ -1,6 +1,6 @@
server {
listen 80;
- server_name _;
+ server_name admin-vpn.nesterovic.cc;
location /api/ {
proxy_pass http://backend:8080;
@@ -13,6 +13,29 @@ server {
location / {
proxy_pass http://admin-web:80;
proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+
+server {
+ listen 80;
+ server_name vpn.nesterovic.cc _;
+
+ location /api/ {
+ proxy_pass http://backend:8080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location / {
+ proxy_pass http://public-web:80;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
diff --git a/public-web/Dockerfile b/public-web/Dockerfile
new file mode 100644
index 0000000..1cddda0
--- /dev/null
+++ b/public-web/Dockerfile
@@ -0,0 +1,8 @@
+FROM nginx:1.27-alpine
+
+COPY public-web/index.html /usr/share/nginx/html/index.html
+COPY public-web/styles.css /usr/share/nginx/html/styles.css
+COPY admin-web/public/NexaVPN_Logo.png /usr/share/nginx/html/NexaVPN_Logo.png
+COPY admin-web/public/NexaVPN_Logo_Only.png /usr/share/nginx/html/NexaVPN_Logo_Only.png
+COPY admin-web/public/favicon.ico /usr/share/nginx/html/favicon.ico
+COPY public-web/nginx.conf /etc/nginx/conf.d/default.conf
diff --git a/public-web/index.html b/public-web/index.html
new file mode 100644
index 0000000..e2c8532
--- /dev/null
+++ b/public-web/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ NexaVPN
+
+
+
+
+
+
+
+ Private access
+ Connect with the NexaVPN app.
+
+ Use the desktop client to sign in, provision this device, and connect to your private network.
+
+
+
+
+
+ What this host is for
+
+ - Desktop client login and device enrollment
+ - Profile sync for provisioned devices
+ - Public VPN entrypoint information
+
+
+
+
+
diff --git a/public-web/nginx.conf b/public-web/nginx.conf
new file mode 100644
index 0000000..9a7833a
--- /dev/null
+++ b/public-web/nginx.conf
@@ -0,0 +1,10 @@
+server {
+ listen 80;
+ server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri /index.html;
+ }
+}
diff --git a/public-web/styles.css b/public-web/styles.css
new file mode 100644
index 0000000..4e8c343
--- /dev/null
+++ b/public-web/styles.css
@@ -0,0 +1,126 @@
+:root {
+ color-scheme: dark;
+ font-family: "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif;
+ --bg: #08111d;
+ --panel: rgba(15, 24, 41, 0.82);
+ --text: #eef4ff;
+ --muted: #a7b7d4;
+ --line: rgba(177, 197, 229, 0.14);
+ --accent: #74e0b8;
+ --accent-strong: #1fb67a;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ color: var(--text);
+ background:
+ radial-gradient(circle at top left, rgba(116, 224, 184, 0.17), transparent 25%),
+ radial-gradient(circle at right, rgba(74, 120, 255, 0.12), transparent 20%),
+ linear-gradient(180deg, #07101c 0%, #0d1728 100%);
+}
+
+.shell {
+ width: min(960px, 100%);
+ margin: 0 auto;
+ min-height: 100vh;
+ display: grid;
+ align-content: center;
+ gap: 24px;
+ padding: 32px 24px;
+}
+
+.hero,
+.card {
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: 28px;
+ box-shadow: 0 24px 72px rgba(3, 8, 20, 0.34);
+ backdrop-filter: blur(16px);
+}
+
+.hero {
+ display: grid;
+ gap: 18px;
+ padding: 32px;
+}
+
+.logo {
+ width: min(280px, 100%);
+ height: auto;
+}
+
+.eyebrow {
+ margin: 0;
+ color: var(--accent);
+ text-transform: uppercase;
+ letter-spacing: 0.18em;
+ font-size: 0.78rem;
+}
+
+h1,
+h2,
+p,
+ul {
+ margin: 0;
+}
+
+h1 {
+ font-size: clamp(2rem, 5vw, 3.4rem);
+ line-height: 1.05;
+}
+
+.copy {
+ max-width: 42rem;
+ color: var(--muted);
+ line-height: 1.6;
+ font-size: 1.05rem;
+}
+
+.actions {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px 18px;
+ border-radius: 999px;
+ text-decoration: none;
+ font-weight: 700;
+}
+
+.button.primary {
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%);
+ color: #04141a;
+}
+
+.button.secondary {
+ border: 1px solid var(--line);
+ color: var(--text);
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.card {
+ padding: 24px 28px;
+}
+
+.card ul {
+ padding-left: 20px;
+ color: var(--muted);
+ line-height: 1.8;
+}
+
+@media (max-width: 720px) {
+ .hero,
+ .card {
+ padding: 24px;
+ }
+}