Skip to content

Commit 2162c29

Browse files
BryonLewisBryon Lewis
andauthored
Dynamic Subpath Support for Deployment (#197)
* support dynamic subpath configs * minio django access for prod deployment * linting --------- Co-authored-by: Bryon Lewis <[email protected]>
1 parent 6985795 commit 2162c29

File tree

9 files changed

+187
-29
lines changed

9 files changed

+187
-29
lines changed

bats_ai/settings.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def DATABASES(self): # noqa
7070

7171
class DevelopmentConfiguration(BatsAiMixin, DevelopmentBaseConfiguration):
7272
SECRET_KEY = 'secretkey' # Dummy value for local development configuration
73+
7374
baseHost = 'localhost'
7475
if 'SERVERHOSTNAME' in os.environ:
7576
baseHost = os.environ['SERVERHOSTNAME']
@@ -96,6 +97,12 @@ class TestingConfiguration(BatsAiMixin, TestingBaseConfiguration):
9697

9798

9899
class KitwareConfiguration(BatsAiMixin, _BaseConfiguration):
100+
subpath = os.environ.get('SUBPATH', '').strip('/')
101+
102+
# If SUBPATH is non-empty, prefix URLs accordingly, else use defaults
103+
STATIC_URL = f'/{subpath}/static/' if subpath else '/static/'
104+
LOGIN_URL = f'/{subpath}/accounts/login/' if subpath else '/accounts/login/'
105+
USE_X_FORWARDED_HOST = True # If behind a proxy
99106
SECRET_KEY = values.SecretValue()
100107
baseHost = 'batdetectai.kitware.com'
101108
if 'SERVERHOSTNAME' in os.environ:
@@ -115,7 +122,7 @@ class KitwareConfiguration(BatsAiMixin, _BaseConfiguration):
115122
MINIO_STORAGE_AUTO_CREATE_MEDIA_BUCKET = True
116123
MINIO_STORAGE_AUTO_CREATE_MEDIA_POLICY = 'READ_WRITE'
117124
MINIO_STORAGE_MEDIA_USE_PRESIGNED = True
118-
MINIO_STORAGE_MEDIA_URL = f'http://{baseHost}:9000/django-storage'
125+
MINIO_STORAGE_MEDIA_URL = f'https://{baseHost}/django-storage'
119126
ALLOWED_HOSTS = [baseHost]
120127
CSRF_TRUSTED_ORIGINS = [f'https://{baseHost}', f'https://{baseHost}']
121128
CORS_ORIGIN_WHITELIST = [f'https://{baseHost}', f'https://{baseHost}']
@@ -130,6 +137,12 @@ class HerokuProductionConfiguration(BatsAiMixin, HerokuProductionBaseConfigurati
130137

131138

132139
class AwsProductionConfiguration(BatsAiMixin, _BaseConfiguration):
140+
SUBPATH = os.environ.get('SUBPATH', '').strip('/')
141+
142+
# If SUBPATH is non-empty, prefix URLs accordingly, else use defaults
143+
STATIC_URL = f'/{SUBPATH}/static/' if SUBPATH else '/static/'
144+
LOGIN_URL = f'/{SUBPATH}/accounts/login/' if SUBPATH else '/accounts/login/'
145+
133146
DEFAULT_FILE_STORAGE = 'storages.backends.s3.S3Storage'
134147
baseHost = '[batdetectai.kitware.com](http://batdetectai.kitware.com/)'
135148
if 'SERVERHOSTNAME' in os.environ:

bats_ai/urls.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
from django.conf import settings
24
from django.contrib import admin
35
from django.urls import include, path
@@ -9,15 +11,15 @@
911

1012
from .api import api
1113

12-
# Some more specific Api Requests
1314
# OpenAPI generation
1415
schema_view = get_schema_view(
1516
openapi.Info(title='bats-ai', default_version='v1', description=''),
1617
public=True,
1718
permission_classes=(permissions.AllowAny,),
1819
)
1920

20-
urlpatterns = [
21+
# Core URL patterns
22+
base_urlpatterns = [
2123
path('accounts/', include('allauth.urls')),
2224
path('oauth/', include('oauth2_provider.urls')),
2325
path('admin/', admin.site.urls),
@@ -29,6 +31,14 @@
2931
path('', include('django_large_image.urls')),
3032
]
3133

34+
# Add subpath prefix if SUBPATH is defined
35+
subpath = os.environ.get('SUBPATH', '').strip('/')
36+
if subpath:
37+
urlpatterns = [path(f'{subpath}/', include(base_urlpatterns))]
38+
else:
39+
urlpatterns = base_urlpatterns
40+
41+
# Add debug toolbar if in DEBUG mode
3242
if settings.DEBUG:
3343
import debug_toolbar
3444

client/src/router/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ function beforeEach(
2626
}
2727
next();
2828
}
29+
const subpath = import.meta.env.VITE_APP_SUBPATH?.replace(/\/+$/, '');
30+
const routerBase = subpath ? `/${subpath}/` : '/';
2931

3032

3133
function routerInit(){
3234
const router = createRouter({
33-
history: createWebHistory(),
35+
base: routerBase,
36+
history: createWebHistory(routerBase),
3437
routes: [
3538
{
3639
path: '/',

client/vite.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import path from 'path';
22
import { defineConfig } from 'vite';
33
import Vue from '@vitejs/plugin-vue';
44
import Vuetify from 'vite-plugin-vuetify';
5+
const subpath = process.env.VITE_APP_SUBPATH || './';
56

67
// https://vitejs.dev/config/
78
export default defineConfig({
9+
base: subpath.endsWith('/') ? subpath : `${subpath}/`,
810
envPrefix: 'VITE_APP_',
911
define: {
1012
// Populated by netlify https://docs.netlify.com/configure-builds/environment-variables/

dev/.env.prod.docker-compose.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ VITE_APP_API_ROOT=https://batdetectai.kitware.com/api/v1
2020
VITE_APP_OAUTH_API_ROOT=https://batdetectai.kitware.com/oauth/
2121
VITE_APP_OAUTH_CLIENT_ID=HSJWFZ2cIpWQOvNyCXyStV9hiOd7DfWeBOCzo4pP
2222
VITE_APP_LOGIN_REDIRECT=https://batdetectai.kitware.com
23+
SUBPATH=

dev/client.Dockerfile

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,73 @@
1-
# Use official Node.js image as the base image for building Vue.js app
2-
FROM node:18 as build-stage
1+
# ========================
2+
# Build Stage (Vue + Env)
3+
# ========================
4+
FROM node:18 AS build-stage
35

4-
# Set working directory
56
WORKDIR /app
67

7-
# Copy package.json and package-lock.json
8-
COPY client/package*.json ./
8+
# Build-time args
9+
ARG VITE_APP_API_ROOT
10+
ARG VITE_APP_OAUTH_API_ROOT
11+
ARG VITE_APP_OAUTH_CLIENT_ID
12+
ARG VITE_APP_LOGIN_REDIRECT
13+
ARG SUBPATH
914

10-
# Install dependencies
15+
# Copy and install dependencies
16+
COPY client/package*.json ./
1117
RUN npm install
1218

13-
# Copy the rest of the application
19+
# Copy full client code
1420
COPY client .
1521

16-
# Build the Vue.js application
22+
# Log and write .env.production
23+
RUN echo "SUBPATH=${SUBPATH}" && \
24+
echo "VITE_APP_API_ROOT=${VITE_APP_API_ROOT}" >> .env.production && \
25+
echo "VITE_APP_OAUTH_API_ROOT=${VITE_APP_OAUTH_API_ROOT}" >> .env.production && \
26+
echo "VITE_APP_OAUTH_CLIENT_ID=${VITE_APP_OAUTH_CLIENT_ID}" >> .env.production && \
27+
echo "VITE_APP_LOGIN_REDIRECT=${VITE_APP_LOGIN_REDIRECT}" >> .env.production && \
28+
echo "VITE_APP_SUBPATH=${SUBPATH}" >> .env.production
29+
30+
# Set environment for build
31+
ENV VITE_APP_API_ROOT=${VITE_APP_API_ROOT}
32+
ENV VITE_APP_OAUTH_API_ROOT=${VITE_APP_OAUTH_API_ROOT}
33+
ENV VITE_APP_OAUTH_CLIENT_ID=${VITE_APP_OAUTH_CLIENT_ID}
34+
ENV VITE_APP_LOGIN_REDIRECT=${VITE_APP_LOGIN_REDIRECT}
35+
ENV SUBPATH=${SUBPATH}
36+
ENV VITE_APP_SUBPATH=${SUBPATH}
37+
38+
# Run Vue build
1739
RUN npm run build
1840

19-
# Use NGINX as the final base image
20-
FROM nginx:alpine
41+
# ========================
42+
# Nginx Stage
43+
# ========================
44+
FROM nginx:alpine AS production-stage
2145

22-
# Remove default NGINX website
46+
# Build-time ARG to get SUBPATH
47+
ARG SUBPATH
48+
49+
# Clean default site
2350
RUN rm -rf /usr/share/nginx/html/*
2451

25-
# Copy built Vue.js app to NGINX HTML directory
26-
COPY --from=build-stage /app/dist /usr/share/nginx/html
52+
# Copy build output
53+
COPY --from=build-stage /app/dist /tmp/dist
2754

28-
RUN ls
29-
# Copy custom NGINX configuration
30-
COPY nginx/nginx.conf /etc/nginx/nginx.conf
3155

32-
# Expose port 80
33-
EXPOSE 80
56+
# If SUBPATH is set, copy dist to subfolder and rewrite nginx config
57+
COPY nginx/nginx.subpath.template /nginx.subpath.template
58+
COPY nginx/nginx.conf /nginx.conf
3459

35-
# Start NGINX
60+
# hadolint ignore=SC2016
61+
RUN if [ -n "$SUBPATH" ]; then \
62+
echo "Copying Vue build to /usr/share/nginx/html/${SUBPATH}"; \
63+
mkdir -p /usr/share/nginx/html/${SUBPATH}; \
64+
cp -r /tmp/dist/* /usr/share/nginx/html/${SUBPATH}/; \
65+
envsubst '${SUBPATH}' < /nginx.subpath.template > /etc/nginx/nginx.conf; \
66+
else \
67+
echo "No SUBPATH set. Using default nginx.conf"; \
68+
cp /nginx.conf /etc/nginx/nginx.conf; \
69+
cp -r /tmp/dist/* /usr/share/nginx/html/; \
70+
fi
71+
72+
EXPOSE 80
3673
CMD ["nginx", "-g", "daemon off;"]

dev/django.Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ ARG BUILD_ENV
3939

4040
# If not bind mounted we need bats_ai for celery deployment
4141
# The bind mount will override this directory
42-
COPY ./bats_ai /opt/django-project/
42+
COPY ./ /opt/django-project/
4343

4444
# hadolint ignore=DL3013
4545
RUN set -ex \

docker-compose.prod.yml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ services:
5151
# command: ""
5252
# Log printing via Rich is enhanced by a TTY
5353
tty: true
54-
env_file: ./dev/.env.prod.docker-compose
54+
env_file:
55+
- ./dev/.env.prod.docker-compose
5556
networks:
5657
- django-nginx
5758
volumes:
@@ -80,7 +81,8 @@ services:
8081
]
8182
# Docker Compose does not set the TTY width, which causes Celery errors
8283
tty: false
83-
env_file: ./dev/.env.prod.docker-compose
84+
env_file:
85+
- ./dev/.env.prod.docker-compose
8486
networks:
8587
- django-nginx
8688
depends_on:
@@ -94,7 +96,14 @@ services:
9496
build:
9597
context: .
9698
dockerfile: ./dev/client.Dockerfile
97-
env_file: ./dev/.env.prod.docker-compose
99+
args:
100+
VITE_APP_API_ROOT: ${VITE_APP_API_ROOT}
101+
VITE_APP_OAUTH_API_ROOT: ${VITE_APP_OAUTH_API_ROOT}
102+
VITE_APP_OAUTH_CLIENT_ID: ${VITE_APP_OAUTH_CLIENT_ID}
103+
VITE_APP_LOGIN_REDIRECT: ${VITE_APP_LOGIN_REDIRECT}
104+
SUBPATH: ${SUBPATH}
105+
env_file:
106+
- ./dev/.env.prod.docker-compose
98107
networks:
99108
- django-nginx
100109
depends_on:
@@ -118,7 +127,8 @@ services:
118127
- postgres:/var/lib/postgresql/data
119128

120129
rabbitmq:
121-
env_file: ./dev/.env.prod.docker-compose
130+
env_file:
131+
- ./dev/.env.prod.docker-compose
122132
image: rabbitmq:management
123133
networks:
124134
- django-nginx
@@ -133,7 +143,8 @@ services:
133143
# When run with a TTY, minio prints credentials on startup
134144
tty: true
135145
command: ["server", "/data", "--console-address", ":${DOCKER_MINIO_CONSOLE_PORT-9001}"]
136-
env_file: ./dev/.env.prod.docker-compose
146+
env_file:
147+
- ./dev/.env.prod.docker-compose
137148
environment:
138149
- MINIO_ROOT_USER=${DJANGO_MINIO_STORAGE_ACCESS_KEY:-minioAccessKey}
139150
- MINIO_ROOT_PASSWORD=${DJANGO_MINIO_STORAGE_SECRET_KEY:-minioSecretKey}

nginx/nginx.subpath.template

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
worker_processes 1;
2+
3+
events {
4+
worker_connections 1024;
5+
}
6+
7+
http {
8+
include /etc/nginx/mime.types;
9+
default_type application/octet-stream;
10+
client_max_body_size 100m;
11+
12+
server {
13+
listen 80;
14+
15+
# Serve Vue.js static files
16+
17+
location /${SUBPATH} {
18+
root /usr/share/nginx/html;
19+
index index.html;
20+
try_files $uri $uri/ /${SUBPATH}/index.html;
21+
}
22+
23+
location /${SUBPATH}/api {
24+
proxy_pass http://django:8000/${SUBPATH}/api;
25+
proxy_set_header Host $host;
26+
proxy_set_header X-Real-IP $remote_addr;
27+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
28+
proxy_set_header X-Forwarded-Proto $scheme;
29+
proxy_redirect off;
30+
}
31+
32+
location /${SUBPATH}/oauth {
33+
proxy_pass http://django:8000/${SUBPATH}/oauth;
34+
proxy_set_header Host $host;
35+
proxy_set_header X-Real-IP $remote_addr;
36+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
37+
proxy_set_header X-Forwarded-Proto $scheme;
38+
proxy_redirect off;
39+
}
40+
41+
location /${SUBPATH}/static {
42+
proxy_pass http://django:8000/${SUBPATH}/static;
43+
proxy_set_header Host $host;
44+
proxy_set_header X-Real-IP $remote_addr;
45+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
46+
proxy_set_header X-Forwarded-Proto $scheme;
47+
proxy_redirect off;
48+
}
49+
50+
location /${SUBPATH}/admin {
51+
proxy_pass http://django:8000/${SUBPATH}/admin;
52+
proxy_set_header Host $host;
53+
proxy_set_header X-Real-IP $remote_addr;
54+
proxy_set_header X-SCRIPT-NAME /${SUBPATH}/;
55+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
56+
proxy_set_header X-Forwarded-Proto $scheme;
57+
proxy_redirect off;
58+
}
59+
60+
61+
62+
location /${SUBPATH}/accounts {
63+
proxy_pass http://django:8000/${SUBPATH}/accounts;
64+
proxy_set_header Host $host;
65+
proxy_set_header X-Real-IP $remote_addr;
66+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
67+
proxy_set_header X-Forwarded-Proto $scheme;
68+
proxy_redirect off;
69+
}
70+
71+
location /django-storage {
72+
proxy_pass http://minio:9000/django-storage;
73+
proxy_set_header Host $host;
74+
proxy_set_header X-Real-IP $remote_addr;
75+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
76+
proxy_set_header X-Forwarded-Proto $scheme;
77+
proxy_redirect off;
78+
}
79+
80+
}
81+
}

0 commit comments

Comments
 (0)