diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..538aca0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# Frontend build output +frontend/.gitignore +frontend/.svelte-kit +frontend/build +frontend/node_modules +frontend/package +frontend/vite.config.js.timestamp-* +frontend/vite.config.ts.timestamp-* + +# Backend cache files and virtualenv +backend/.venv/ +*.pyc +__pycache__/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c7151d --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Local database +/.postgres/ + +# Latex compiled files +**/*.aux +**/*.bbl +**/*.bcf +**/*.blg +**/*.gz +**/*.log +**/*.out +**/*.run.xml +**/*.pdf + +# Drawio backup and lock files +**/*.drawio.bkp +**/*.drawio.dtmp diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..f362caf --- /dev/null +++ b/Caddyfile @@ -0,0 +1,14 @@ +:8000 { + handle_path /api/* { + reverse_proxy * todo-backend:3000 + } + + handle * { + reverse_proxy * todo-frontend:3000 + } + + log { + output stderr + format console + } +} diff --git a/development.backend.Dockerfile b/development.backend.Dockerfile new file mode 100644 index 0000000..2a88dad --- /dev/null +++ b/development.backend.Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:1 + +FROM python:alpine + +# Create non-root user +ARG CUSTOM_UID +ARG CUSTOM_GID +ENV CUSTOM_USERNAME=backend +ENV CUSTOM_GROUPNAME=backend +RUN addgroup --gid ${CUSTOM_GID:-1000} ${CUSTOM_GROUPNAME} && \ + adduser --uid ${CUSTOM_UID:-1000} --shell /bin/ash ${CUSTOM_USERNAME} --ingroup ${CUSTOM_GROUPNAME} --disabled-password && \ + mkdir /app && chown ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} /app && chmod 700 /app +ENV PATH "$PATH:/home/${CUSTOM_GROUPNAME}/.local/bin" + +# Copy source files +WORKDIR /app +COPY --chown=${CUSTOM_USERNAME}:${CUSTOM_GROUPNAME} backend/ /app/ + +# Install dependencies +USER ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} +RUN pip install -r requirements.txt + +# Run ASGI server +EXPOSE 3000/tcp +ENTRYPOINT ["uvicorn", "todo.main:app", "--root-path", "/api", "--host", "0.0.0.0", "--port", "3000", "--access-log", "--use-colors", "--log-level", "debug", "--reload"] diff --git a/development.docker-compose.yml b/development.docker-compose.yml new file mode 100644 index 0000000..839d5be --- /dev/null +++ b/development.docker-compose.yml @@ -0,0 +1,93 @@ +--- + +version: "3" + +services: + todo-webserver: + container_name: todo-webserver + restart: unless-stopped + build: + context: . + dockerfile: development.webserver.Dockerfile + args: + CUSTOM_UID: 1000 + CUSTOM_GID: 1000 + environment: + TZ: Europe/Berlin + ports: + - "8000:8000" + volumes: + - ./Caddyfile:/app/Caddyfile + todo-frontend: + container_name: todo-frontend + restart: unless-stopped + depends_on: + - todo-webserver + build: + context: . + dockerfile: development.frontend.Dockerfile + args: + CUSTOM_UID: 1000 + CUSTOM_GID: 1000 + environment: + TZ: Europe/Berlin + expose: + - "3000" + volumes: + - ./frontend/postcss.config.js:/app/postcss.config.js:ro + - ./frontend/svelte.config.js:/app/svelte.config.js:ro + - ./frontend/tailwind.config.js:/app/tailwind.config.js:ro + - ./frontend/tsconfig.json:/app/tsconfig.json:ro + - ./frontend/vite.config.ts:/app/vite.config.ts:ro + - ./frontend/src:/app/src:ro + - ./frontend/static:/app/static:ro + todo-backend: + container_name: todo-backend + restart: unless-stopped + depends_on: + - todo-webserver + - todo-db + build: + context: . + dockerfile: ./development.backend.Dockerfile + args: + CUSTOM_UID: 1000 + CUSTOM_GID: 1000 + expose: + - "3000" + volumes: + - ./backend/todo/:/app/todo:ro + - ./backend/requirements.txt:/app/requirements.txt:ro + environment: + APP_NAME: "TodoApp" + ADMIN_EMAIL: "admin@example.com" + DEBUG_MODE: "true" + POSTGRES_HOST: "todo-db" + POSTGRES_PORT: "5432" + POSTGRES_DB: "todo" + POSTGRES_USER: "todo" + POSTGRES_PASSWORD: "todo" + todo-db: + image: postgres:alpine + container_name: todo-db + restart: unless-stopped + expose: + - "5432" + volumes: + - ./.postgres:/var/lib/postgresql/data + environment: + POSTGRES_DB: "todo" + POSTGRES_USER: "todo" + POSTGRES_PASSWORD: "todo" + todo-pgweb: + image: sosedoff/pgweb + container_name: todo-pgweb + restart: unless-stopped + depends_on: + - todo-db + ports: + - "8001:8081" + environment: + DATABASE_URL: "postgres://todo:todo@todo-db:5432/todo?sslmode=disable" + +... diff --git a/development.frontend.Dockerfile b/development.frontend.Dockerfile new file mode 100644 index 0000000..4e4f0fa --- /dev/null +++ b/development.frontend.Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1 + +FROM alpine:latest + +# Install npm and nodejs +RUN apk add --no-cache nodejs npm + +# Create non-root user +ARG CUSTOM_UID +ARG CUSTOM_GID +ENV CUSTOM_USERNAME=frontend +ENV CUSTOM_GROUPNAME=frontend +RUN addgroup --gid ${CUSTOM_GID:-1000} ${CUSTOM_GROUPNAME} && \ + adduser --uid ${CUSTOM_UID:-1000} --shell /bin/ash ${CUSTOM_USERNAME} --ingroup ${CUSTOM_GROUPNAME} --disabled-password && \ + mkdir /app && chown ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} /app && chmod 700 /app + +# Copy source files +COPY --chown=${CUSTOM_USERNAME}:${CUSTOM_GROUPNAME} frontend/ /app/ + +# Install dependencies +USER ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} +WORKDIR /app/ +RUN npm install + +# Run vite dev server +ENV NODE_ENV=development +EXPOSE 3000 +ENTRYPOINT ["npm", "run", "dev"] diff --git a/development.webserver.Dockerfile b/development.webserver.Dockerfile new file mode 100644 index 0000000..d1d307f --- /dev/null +++ b/development.webserver.Dockerfile @@ -0,0 +1,24 @@ +# syntax=docker/dockerfile:1 + +FROM alpine:latest + +# Install caddy +RUN apk add --no-cache caddy + +# Create non-root user +ARG CUSTOM_UID +ARG CUSTOM_GID +ENV CUSTOM_USERNAME=webserver +ENV CUSTOM_GROUPNAME=webserver +RUN addgroup --gid ${CUSTOM_GID:-1000} ${CUSTOM_GROUPNAME} && \ + adduser --uid ${CUSTOM_UID:-1000} --shell /bin/ash ${CUSTOM_USERNAME} --ingroup ${CUSTOM_GROUPNAME} --disabled-password && \ + mkdir /app && chown ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} /app && chmod 700 /app + +# Copy caddy config +WORKDIR /app +COPY --chown=${CUSTOM_USERNAME}:${CUSTOM_GROUPNAME} Caddyfile /app/ + +# Run Caddy in development mode +USER ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} +EXPOSE 8000 +ENTRYPOINT ["caddy", "run", "--config", "/app/Caddyfile", "--adapter", "caddyfile", "--watch"] diff --git a/frontend/.env b/frontend/.env index 9c70025..e165fb3 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1,3 +1,22 @@ +# Environment variable defaults for node and sveltekit +# NOTE: variables prefixed with 'PUBLIC_' are visible to clients! + PUBLIC_TITLE="Example TODO App" PUBLIC_DESCRIPTION="An example TODO app built with sveltekit and fastapi." PUBLIC_AUTHOR="John Doe" + +HOST="0.0.0.0" +PORT="3000" + +# WARNING: only set these if running behind a trusted reverse proxy! +# See `https://kit.svelte.dev/docs/adapter-node#environment-variables-origin-protocol-header-and-host-header`. +ORIGIN=http://localhost +PROTOCOL_HEADER="x-forwarded-proto" +HOST_HEADER="x-forwarded-host" + +# See `https://kit.svelte.dev/docs/adapter-node#environment-variables-address-header-and-xff-depth`. +ADDRESS_HEADER="True-Client-IP" + +# Maximum request body size to accept in bytes. +# See `https://kit.svelte.dev/docs/adapter-node#environment-variables-body-size-limit`. +BODY_SIZE_LIMIT="1048576" diff --git a/frontend/.env.development b/frontend/.env.development index 1762eb8..db1ebd5 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1 +1,6 @@ +# See `https://kit.svelte.dev/docs/adapter-node#environment-variables-origin-protocol-header-and-host-header`. ORIGIN=http://localhost + +# See `https://kit.svelte.dev/docs/adapter-node#environment-variables-address-header-and-xff-depth`. +ADDRESS_HEADER="X-Forwarded-For" +XFF_DEPTH="1" diff --git a/frontend/README.md b/frontend/README.md index a24bcdb..fe7aaac 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -4,21 +4,16 @@ This a sveltekit project, created with the npm creation script. ## Developing -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: +Start the development Docker stack by running ```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open +sudo docker-compose -f development.docker-compose.yml up --build --force-recreate --remove-orphans ``` ## Building -To create a production version of your app: +To run the development Docker stack, run ```bash -npm run build +sudo docker-compose -f production.docker-compose.yml up --build --force-recreate --remove-orphans --detach ``` - -You can preview the production build with `npm run preview`. diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 0fe5e54..0e766c1 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -7,7 +7,7 @@ - + @@ -25,11 +25,11 @@ import "../app.css"; import { page } from '$app/stores'; - import ogImage from '/images/common/og-image.webp'; - export let title = import.meta.env.PUBLIC_TITLE; export let description = import.meta.env.PUBLIC_DESCRIPTION; export let author = import.meta.env.PUBLIC_AUTHOR; + + const ogImageUrl = new URL('/images/common/og-image.webp', import.meta.url).href diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 901c39b..778053c 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,2 +1,6 @@ + +

Hello World!

This is a simple Todo-App as a tech stack template for new projects.

diff --git a/frontend/src/routes/todo/+page.svelte b/frontend/src/routes/todo/+page.svelte index a66608a..ac36072 100644 --- a/frontend/src/routes/todo/+page.svelte +++ b/frontend/src/routes/todo/+page.svelte @@ -1,11 +1,27 @@

TODOs

-

Currently, there are {count} todo-items

+{#await promise} +

Waiting

+{:then users} +

{users}

+{:catch error} +

{error}

+{/await} diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js index e7737a8..685f03d 100644 --- a/frontend/svelte.config.js +++ b/frontend/svelte.config.js @@ -6,7 +6,10 @@ const config = { preprocess: vitePreprocess(), kit: { - adapter: adapter() + adapter: adapter({ + // Enable gzip and brotli compression of static assets and precompiled documents if not in development + precompress: process.env.NODE_ENV !== 'development' + }) } }; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 485bf59..dff31fd 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,4 +4,14 @@ import { defineConfig } from 'vite'; export default defineConfig({ envPrefix: 'PUBLIC_', plugins: [sveltekit()], + server: { + host: '0.0.0.0', + port: 3000, + strictPort: true, + }, + preview: { + host: '0.0.0.0', + port: 3000, + strictPort: true, + }, }); diff --git a/production.backend.Dockerfile b/production.backend.Dockerfile new file mode 100644 index 0000000..4079fa4 --- /dev/null +++ b/production.backend.Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:1 + +FROM python:alpine + +# Create non-root user +ARG CUSTOM_UID +ARG CUSTOM_GID +ENV CUSTOM_USERNAME=backend +ENV CUSTOM_GROUPNAME=backend +RUN addgroup --gid ${CUSTOM_GID:-1000} ${CUSTOM_GROUPNAME} && \ + adduser --uid ${CUSTOM_UID:-1000} --shell /bin/ash ${CUSTOM_USERNAME} --ingroup ${CUSTOM_GROUPNAME} --disabled-password && \ + mkdir /app && chown ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} /app && chmod 700 /app +ENV PATH "$PATH:/home/${CUSTOM_GROUPNAME}/.local/bin" + +# Copy source files +WORKDIR /app +COPY --chown=${CUSTOM_USERNAME}:${CUSTOM_GROUPNAME} backend/ /app/ + +# Install dependencies +USER ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} +RUN pip install -r requirements.txt + +# Run ASGI server +EXPOSE 3000/tcp +ENTRYPOINT ["uvicorn", "todo.main:app", "--root-path", "/api", "--host", "0.0.0.0", "--port", "3000", "--access-log"] diff --git a/production.docker-compose.yml b/production.docker-compose.yml new file mode 100644 index 0000000..f3d6422 --- /dev/null +++ b/production.docker-compose.yml @@ -0,0 +1,88 @@ +--- + +version: "3" + +services: + todo-webserver: + container_name: todo-webserver + restart: unless-stopped + build: + context: . + dockerfile: production.webserver.Dockerfile + args: + CUSTOM_UID: 1000 + CUSTOM_GID: 1000 + networks: + - proxy + - todo + labels: + - "traefik.enable=true" + - "traefik.http.routers.todo.entrypoints=https" + - "traefik.http.routers.todo.rule=Host(`todo.example.com`)" + - "traefik.http.routers.todo-secure.middlewares=default@file" + - "traefik.http.routers.todo.tls=true" + - "traefik.http.services.todo.loadbalancer.server.port=8000" + - 'traefik.docker.network=proxy' + todo-frontend: + container_name: todo-frontend + restart: unless-stopped + depends_on: + - todo-webserver + build: + context: . + dockerfile: production.frontend.Dockerfile + args: + CUSTOM_UID: 1000 + CUSTOM_GID: 1000 + networks: + - todo + expose: + - "3000" + todo-backend: + container_name: todo-backend + restart: unless-stopped + depends_on: + - todo-webserver + - todo-db + build: + context: . + dockerfile: ./production.backend.Dockerfile + args: + CUSTOM_UID: 1000 + CUSTOM_GID: 1000 + networks: + - todo + expose: + - "3000" + environment: + APP_NAME: "TodoApp" + ADMIN_EMAIL: "admin@example.com" + DEBUG_MODE: "true" + POSTGRES_HOST: "todo-db" + POSTGRES_PORT: "5432" + POSTGRES_DB: "todo" + POSTGRES_USER: "todo" + POSTGRES_PASSWORD: "todo" + todo-db: + image: postgres:alpine + container_name: todo-db + restart: unless-stopped + networks: + - todo + expose: + - "5432" + volumes: + - /srv/todo/data:/var/lib/postgresql/data + environment: + TZ: Europe/Berlin + POSTGRES_DB: "todo" + POSTGRES_USER: "todo" + POSTGRES_PASSWORD: "todo" + +networks: + proxy: + external: true + todo: + external: false + +... diff --git a/production.frontend.Dockerfile b/production.frontend.Dockerfile new file mode 100644 index 0000000..7ba5a77 --- /dev/null +++ b/production.frontend.Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 + +FROM alpine:latest + +# Install npm and nodejs +RUN apk add --no-cache nodejs npm + +# Create non-root user +ARG CUSTOM_UID +ARG CUSTOM_GID +ENV CUSTOM_USERNAME=frontend +ENV CUSTOM_GROUPNAME=frontend +RUN addgroup --gid ${CUSTOM_GID:-1000} ${CUSTOM_GROUPNAME} && \ + adduser --uid ${CUSTOM_UID:-1000} --shell /bin/ash ${CUSTOM_USERNAME} --ingroup ${CUSTOM_GROUPNAME} --disabled-password && \ + mkdir /app && chown ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} /app && chmod 700 /app + +# Copy source files +COPY --chown=${CUSTOM_USERNAME}:${CUSTOM_GROUPNAME} frontend/ /app/ + +# Install dependencies and build app +USER ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} +WORKDIR /app/ +RUN npm install +RUN npm run build + +# Run node.js +ENV NODE_ENV=production +EXPOSE 3000 +ENTRYPOINT ["node", "-r", "dotenv/config", "build"] diff --git a/production.webserver.Dockerfile b/production.webserver.Dockerfile new file mode 100644 index 0000000..11370fe --- /dev/null +++ b/production.webserver.Dockerfile @@ -0,0 +1,24 @@ +# syntax=docker/dockerfile:1 + +FROM alpine:latest + +# Install caddy +RUN apk add --no-cache caddy + +# Create non-root user +ARG CUSTOM_UID +ARG CUSTOM_GID +ENV CUSTOM_USERNAME=webserver +ENV CUSTOM_GROUPNAME=webserver +RUN addgroup --gid ${CUSTOM_GID:-1000} ${CUSTOM_GROUPNAME} && \ + adduser --uid ${CUSTOM_UID:-1000} --shell /bin/ash ${CUSTOM_USERNAME} --ingroup ${CUSTOM_GROUPNAME} --disabled-password && \ + mkdir /app && chown ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} /app && chmod 700 /app + +# Copy caddy config +WORKDIR /app +COPY --chown=${CUSTOM_USERNAME}:${CUSTOM_GROUPNAME} Caddyfile /app/ + +# Run Caddy in development mode +USER ${CUSTOM_UID:-1000}:${CUSTOM_GID:-1000} +EXPOSE 8000 +ENTRYPOINT ["caddy", "run", "--config", "/app/Caddyfile", "--adapter", "caddyfile"]