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
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 @@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"]