feat(deploy): single-container Docker packaging (nginx + Flask + prebuilt SPA)
Add self-contained Docker deployment for EasyPanel / docker-compose: - Dockerfile: python:3.11-slim + nginx. Serves the PREBUILT Vue SPA (frontend/dist, committed) via nginx and reverse-proxies /api to the Flask backend on 127.0.0.1:5000. No node/npm in the image (avoids Vite/npm flakiness in Docker builds). VOLUME /app/backend/data for persistence (factor vintages, dividend ledger, forward runs, prices). HEALTHCHECK hits /api/v1/dashboard/summary. Exposes :80. - deploy/nginx.conf: SPA fallback + /api proxy to 127.0.0.1:5000 + gzip. - deploy/entrypoint.sh: starts Flask (HOST=0.0.0.0:5000) + nginx foreground, forwards signals. - docker-compose.yml: single service 'set50', port 8080:80, mounts ./backend/data for persistence, healthcheck. - .dockerignore excludes .git/.venv/backend/data/node_modules/docs. - .gitignore now tracks frontend/dist/ (prebuilt bundle required by the image); backend/data stays untracked. - Backend runtime verified: test_client GET /api/v1/dashboard/summary=200 (the HEALTHCHECK path). entrypoint sh -n, compose YAML parse, and all Dockerfile-referenced files exist. NOTE: no local docker here, so the image itself was not built — that happens on EasyPanel.
This commit is contained in:
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.git
|
||||||
|
.venv
|
||||||
|
.hermes
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache
|
||||||
|
backend/.pytest_cache
|
||||||
|
backend/data
|
||||||
|
backend/.venv
|
||||||
|
frontend/node_modules
|
||||||
|
reports
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
docs
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,5 +6,6 @@ __pycache__/
|
|||||||
backend/.pytest_cache/
|
backend/.pytest_cache/
|
||||||
backend/data/
|
backend/data/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/dist/
|
# note: frontend/dist/ is intentionally TRACKED (prebuilt static bundle used by
|
||||||
|
# the Docker image — see Dockerfile). Rebuild with `cd frontend && npm run build`.
|
||||||
reports/
|
reports/
|
||||||
|
|||||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# SET50 alternative-data platform - single-container image.
|
||||||
|
# Serves: built Vue SPA (frontend/dist) via nginx, proxying /api to the
|
||||||
|
# Flask backend (127.0.0.1:5000). Data persists under /app/backend/data
|
||||||
|
# (mount a volume there or set DATA_DIR to a mounted path).
|
||||||
|
#
|
||||||
|
# Build strategy: PREBUILT frontend dist (no node/npm/vite in this image) —
|
||||||
|
# build with `cd frontend && npm run build` and commit frontend/dist/.
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# nginx for serving the SPA
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends nginx \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf 2>/dev/null || true
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 1) frontend static (prebuilt) — must exist on-disk (committed)
|
||||||
|
COPY frontend/dist/ ./frontend/dist/
|
||||||
|
|
||||||
|
# 2) backend deps + code
|
||||||
|
COPY backend/requirements.txt ./backend/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r backend/requirements.txt
|
||||||
|
COPY backend/ ./backend/
|
||||||
|
|
||||||
|
# 3) deploy config (nginx conf + entrypoint)
|
||||||
|
COPY deploy/nginx.conf /etc/nginx/conf.d/set50.conf
|
||||||
|
COPY deploy/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
# persistent data (factor vintages, dividend ledger, forward runs, prices)
|
||||||
|
ENV DATA_DIR=/app/backend/data
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV PORT=5000
|
||||||
|
VOLUME ["/app/backend/data"]
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1/api/v1/dashboard/summary', timeout=4).status==200 else 1)"
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
26
deploy/entrypoint.sh
Normal file
26
deploy/entrypoint.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Start the Flask backend on 0.0.0.0:5000 (bind all interfaces so nginx can
|
||||||
|
# proxy to it inside the container). DATA_DIR is /app/backend/data by default
|
||||||
|
# (a mounted volume makes it survive container recreate).
|
||||||
|
cd /app/backend
|
||||||
|
export HOST="${HOST:-0.0.0.0}"
|
||||||
|
export PORT="${PORT:-5000}"
|
||||||
|
export PAPER_BIND_HOST="$HOST"
|
||||||
|
|
||||||
|
# pid1 helper: run nginx in foreground + Flask subprocess so the container
|
||||||
|
# stays alive and signals are handled. Keep it simple and robust.
|
||||||
|
|
||||||
|
# Start Flask in the background
|
||||||
|
python run.py &
|
||||||
|
FLASK_PID=$!
|
||||||
|
|
||||||
|
# Start nginx in the foreground (its own master + workers)
|
||||||
|
nginx -g "daemon off;" &
|
||||||
|
NGINX_PID=$!
|
||||||
|
|
||||||
|
# forward signals to both and wait
|
||||||
|
trap 'kill $FLASK_PID $NGINX_PID 2>/dev/null || true' INT TERM
|
||||||
|
wait $FLASK_PID
|
||||||
|
wait $NGINX_PID
|
||||||
29
deploy/nginx.conf
Normal file
29
deploy/nginx.conf
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# nginx: serve the built Vue SPA + reverse-proxy /api to the Flask backend.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /app/frontend/dist;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# SPA fallback: any non-API GET that isn't a real file -> index.html
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API traffic goes to the Flask backend on 127.0.0.1:5000
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:5000;
|
||||||
|
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;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# never serve dotfiles
|
||||||
|
location ~ /\. { deny all; }
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript application/json;
|
||||||
|
}
|
||||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Local/self-hosted single-container run for the SET50 alternative-data platform.
|
||||||
|
# Production (EasyPanel) builds from the Dockerfile directly; this compose file
|
||||||
|
# adds a persistent volume so factor vintages, the dividend ledger, forward runs
|
||||||
|
# and prices survive container recreate.
|
||||||
|
services:
|
||||||
|
set50:
|
||||||
|
build: .
|
||||||
|
image: set50-alternative-data:latest
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
environment:
|
||||||
|
# bind backend on all interfaces inside the container (nginx proxies /api)
|
||||||
|
HOST: "0.0.0.0"
|
||||||
|
PORT: "5000"
|
||||||
|
PAPER_BIND_HOST: "0.0.0.0"
|
||||||
|
# optional: make the dividend refresh faster for demos ()
|
||||||
|
# DIVIDEND_REFRESH_COOLDOWN_SECONDS: "300"
|
||||||
|
volumes:
|
||||||
|
- ./backend/data:/app/backend/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1/api/v1/dashboard/summary', timeout=4).status==200 else 1)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
18
frontend/dist/assets/index-DDItjwYy.js
vendored
Normal file
18
frontend/dist/assets/index-DDItjwYy.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index-z73iDem3.css
vendored
Normal file
1
frontend/dist/assets/index-z73iDem3.css
vendored
Normal file
File diff suppressed because one or more lines are too long
14
frontend/dist/index.html
vendored
Normal file
14
frontend/dist/index.html
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#0b1018" />
|
||||||
|
<title>SET50 Signal Lab</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-DDItjwYy.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-z73iDem3.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user