From 11107c58fb347629d01ac844cebfa688a12c28fa Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Tue, 24 Sep 2024 10:45:19 +0200 Subject: [PATCH] Initial docker deployment setup - add docker-compose - add nginx docker image and nginx conf for frontend - remove .env file from backend folder - instead make settings default values the ones used for local development - replace USER_DATABASE_PATH and MILESTONE_DATABASE_PATH with single DATABASE_PATH folder - add mondey_backend/db folder to use as a default location for databases when running backend locally without docker - add `/api` root_path to API, forward all /api locations in nginx to the backend - add a docker CI job to build production containers and push to ghcr.io - note that nginx config is only http for now --- .github/workflows/docker.yml | 40 ++++++++++++++++ docker-compose.yml | 34 +++++++++++++ frontend/.env | 3 +- frontend/Dockerfile | 22 +++++++++ frontend/nginx.conf | 48 +++++++++++++++++++ frontend/src/routes/admin/+page.svelte | 1 + mondey_backend/.env | 9 ---- mondey_backend/Dockerfile | 3 +- mondey_backend/README.md | 1 - mondey_backend/db/README.md | 1 + .../mondey_backend/databases/milestones.py | 2 +- .../src/mondey_backend/databases/users.py | 4 +- mondey_backend/src/mondey_backend/main.py | 2 +- mondey_backend/src/mondey_backend/settings.py | 13 +++-- 14 files changed, 160 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/docker.yml create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf delete mode 100644 mondey_backend/.env create mode 100644 mondey_backend/db/README.md diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..4f3f4928 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,40 @@ +name: docker containers +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: docker-${{ github.ref }} + cancel-in-progress: true + +jobs: + docker: + runs-on: ubuntu-latest + name: "Docker" + steps: + - uses: actions/checkout@v4 + - run: docker compose build + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + - run: | + echo $MONDEY_DOCKER_IMAGE_TAG + docker compose build + docker compose push + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + MONDEY_DOCKER_IMAGE_TAG: ${{ github.sha }} + - run: | + echo $MONDEY_DOCKER_IMAGE_TAG + docker compose build + docker compose push + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + MONDEY_DOCKER_IMAGE_TAG: "latest" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..2467dc7e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + backend: + image: ghcr.io/ssciwr/mondey_backend:${MONDEY_DOCKER_IMAGE_TAG:-latest} + build: ./mondey_backend + volumes: + - ${STATIC_FILES_PATH:-./static}:/app/static + - ${DATABASE_PATH:-./db}:/app/db + environment: + - SECRET=${SECRET:-} + - STATIC_FILES_PATH=/app/static + - DATABASE_PATH=/app/db + - ENABLE_CORS=${ENABLE_CORS:-false} + - HOST=${HOST:-backend} + - PORT=${PORT:-80} + - RELOAD=${RELOAD:-false} + - LOG_LEVEL=${LOG_LEVEL:-info} + frontend: + image: ghcr.io/ssciwr/mondey_frontend:${MONDEY_DOCKER_IMAGE_TAG:-latest} + build: + context: ./frontend + args: + - MONDEY_API_URL=/api + ports: + - "80:80" + - "443:443" +# volumes: +# - ${MONDEY_SSL_CERT:-./cert.pem}:/MONDEY_ssl_cert.pem +# - ${MONDEY_SSL_KEY:-./key.pem}:/MONDEY_ssl_key.pem +# email: +# image: "boky/postfix" +# environment: +# - ALLOW_EMPTY_SENDER_DOMAINS="true" +# networks: +# - mondey-network diff --git a/frontend/.env b/frontend/.env index 1ea55bf4..1b7bbe80 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1 +1,2 @@ -VITE_MONDEY_API_URL=http://localhost:8000 +# api location for local development: +VITE_MONDEY_API_URL=http://localhost:8000/api diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..d7191075 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,22 @@ +FROM node:22-slim AS builder + +LABEL org.opencontainers.image.source=https://github.com/ssciwr/mondey +LABEL org.opencontainers.image.description="MONDEY frontend production image" + +ARG MONDEY_API_URL + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install -g pnpm && pnpm install + +COPY . . + +RUN echo "VITE_MONDEY_API_URL=${MONDEY_API_URL}" > .env && pnpm run build + +FROM nginx:1.27.1 + +COPY --from=builder /app/build /usr/share/nginx/html + +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 00000000..c6db71d8 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,48 @@ +server { + listen 80; +# listen 443 ssl; +# listen [::]:443 ssl; + http2 on; + server_name localhost; +# ssl_certificate /ssl_cert.pem; +# ssl_certificate_key /ssl_key.pem; + + # Maximum file upload size + client_max_body_size 20M; + + # Improve HTTPS performance with session resumption + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Enable server-side protection against BEAST attacks + ssl_protocols TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384"; + + # Aditional Security Headers + # ref: https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; + + # ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + add_header X-Frame-Options DENY always; + + # ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + add_header X-Content-Type-Options nosniff always; + + # ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection + add_header X-Xss-Protection "1; mode=block" always; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://backend:80; + } +} diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index a07f5f2f..a13edad1 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -17,6 +17,7 @@ } onMount(async () => { + console.log(import.meta.env.VITE_MONDEY_API_URL); updateLanguages(); updateIsLoggedIn(); }); diff --git a/mondey_backend/.env b/mondey_backend/.env deleted file mode 100644 index edd9c03b..00000000 --- a/mondey_backend/.env +++ /dev/null @@ -1,9 +0,0 @@ -SECRET=abc123 -USER_DATABASE_PATH=users.db -MILESTONE_DATABASE_PATH=milestones.db -STATIC_FILES_PATH=static -ENABLE_CORS=True -HOST=localhost -PORT=8000 -RELOAD=True -LOG_LEVEL=info diff --git a/mondey_backend/Dockerfile b/mondey_backend/Dockerfile index a93f2e5a..2a9b253f 100644 --- a/mondey_backend/Dockerfile +++ b/mondey_backend/Dockerfile @@ -2,7 +2,6 @@ FROM python:3.12-slim LABEL org.opencontainers.image.source=https://github.com/ssciwr/mondey-frontend-prototype LABEL org.opencontainers.image.description="MONDEY backend production image" -LABEL org.opencontainers.image.licenses=MIT WORKDIR /app @@ -10,4 +9,4 @@ COPY . . RUN pip install . -CMD ["mondey-backend", "--host", "0.0.0.0", "--port", "80", "--log-level", "info"] +CMD ["mondey-backend"] diff --git a/mondey_backend/README.md b/mondey_backend/README.md index 51d0ab9c..c7bc2251 100644 --- a/mondey_backend/README.md +++ b/mondey_backend/README.md @@ -36,7 +36,6 @@ sqlite> UPDATE user SET is_superuser = 1 WHERE email = 'youremail@yourdomain.com The backend can be configured using environment variables, which can be set in a `.env` file in the working directory where you start the backend. -Default settings for local development are included in [.env](.env). ## Tests diff --git a/mondey_backend/db/README.md b/mondey_backend/db/README.md new file mode 100644 index 00000000..6e4fde4b --- /dev/null +++ b/mondey_backend/db/README.md @@ -0,0 +1 @@ +This is the default folder where the databases will be created and stored when running the backend locally. diff --git a/mondey_backend/src/mondey_backend/databases/milestones.py b/mondey_backend/src/mondey_backend/databases/milestones.py index 67b8246f..8251bd22 100644 --- a/mondey_backend/src/mondey_backend/databases/milestones.py +++ b/mondey_backend/src/mondey_backend/databases/milestones.py @@ -6,7 +6,7 @@ from ..settings import app_settings engine = create_engine( - f"sqlite:///{app_settings.MILESTONE_DATABASE_PATH}", + f"sqlite:///{app_settings.DATABASE_PATH}/milestones.db", connect_args={"check_same_thread": False}, ) diff --git a/mondey_backend/src/mondey_backend/databases/users.py b/mondey_backend/src/mondey_backend/databases/users.py index bc3f1134..b2802bb4 100644 --- a/mondey_backend/src/mondey_backend/databases/users.py +++ b/mondey_backend/src/mondey_backend/databases/users.py @@ -15,7 +15,9 @@ from ..models.users import User from ..settings import app_settings -engine = create_async_engine(f"sqlite+aiosqlite:///{app_settings.USER_DATABASE_PATH}") +engine = create_async_engine( + f"sqlite+aiosqlite:///{app_settings.DATABASE_PATH}/users.db" +) async_session_maker = async_sessionmaker(engine, expire_on_commit=False) diff --git a/mondey_backend/src/mondey_backend/main.py b/mondey_backend/src/mondey_backend/main.py index 3f4cea43..c6edf168 100644 --- a/mondey_backend/src/mondey_backend/main.py +++ b/mondey_backend/src/mondey_backend/main.py @@ -27,7 +27,7 @@ async def lifespan(app: FastAPI): # ensure static files directory exists pathlib.Path(app_settings.STATIC_FILES_PATH).mkdir(parents=True, exist_ok=True) -app = FastAPI(lifespan=lifespan, title="MONDEY API") +app = FastAPI(lifespan=lifespan, title="MONDEY API", root_path="/api") app.include_router(milestones.router) app.include_router(admin.router) app.include_router(users.router) diff --git a/mondey_backend/src/mondey_backend/settings.py b/mondey_backend/src/mondey_backend/settings.py index 6c9c8f0a..dfe22123 100644 --- a/mondey_backend/src/mondey_backend/settings.py +++ b/mondey_backend/src/mondey_backend/settings.py @@ -1,21 +1,20 @@ from __future__ import annotations -import secrets - from pydantic_settings import BaseSettings from pydantic_settings import SettingsConfigDict class AppSettings(BaseSettings): + # this will load settings from environment variables or an .env file if present model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") - SECRET: str = secrets.token_urlsafe(64) - USER_DATABASE_PATH: str = "users.db" - MILESTONE_DATABASE_PATH: str = "milestones.db" + # these defaults are for local development and are used if the environment variables are not set + SECRET: str = "abc123" + DATABASE_PATH: str = "db" STATIC_FILES_PATH: str = "static" - ENABLE_CORS: bool = False + ENABLE_CORS: bool = True HOST: str = "localhost" PORT: int = 8000 - RELOAD: bool = False + RELOAD: bool = True LOG_LEVEL: str = "info"