diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..41b447b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +dist +coverage +.env +.env.* +!.env.example +.git +.github +.idea +.vscode +.direnv +.wave +.claude +.navi +*.log diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3881664 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Build-time secrets for Vite (baked into static bundle). +# Copy to .env (gitignored). Used by docker compose build via args. + +VITE_SUPABASE_URL=https://.supabase.co +VITE_SUPABASE_ANON_KEY= diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8f3dbe4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +# Multi-stage: build the Vite static site, then serve via nginx. + +# ── Build stage ──────────────────────────────────────────────────────────── +FROM node:20-alpine AS build +WORKDIR /app + +# Install dependencies first (cache layer) +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy source and build +COPY . . + +# Vite picks up VITE_* at build time. Pass via --build-arg. +ARG VITE_SUPABASE_URL +ARG VITE_SUPABASE_ANON_KEY +ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL +ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY + +RUN npm run build + +# ── Runtime stage ────────────────────────────────────────────────────────── +FROM nginx:1.27-alpine + +# Static SPA: redirect 404s to index.html so client-side routing works. +RUN printf 'server {\n\ + listen 80;\n\ + server_name _;\n\ + root /usr/share/nginx/html;\n\ + index index.html;\n\ + location / {\n\ + try_files $uri $uri/ /index.html;\n\ + }\n\ + location /health {\n\ + access_log off;\n\ + return 200 "ok\\n";\n\ + add_header Content-Type text/plain;\n\ + }\n\ +}\n' > /etc/nginx/conf.d/default.conf + +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1/health || exit 1 diff --git a/README.md b/README.md index 95535dd..8aefbca 100644 --- a/README.md +++ b/README.md @@ -200,21 +200,61 @@ Coverage reports are generated in the `coverage/` directory with detailed HTML r ## 🚢 Deployment -To build the project for production: +### Static build ```bash npm run build ``` -The output will be generated in the `dist/` directory, which can be deployed to any static web server. +Outputs to `dist/`. Deployable to any static web server. -For GitHub Pages deployment, the configuration is already set up with the base path `/code-crispies/`. +For GitHub Pages, base path `/code-crispies/` is preconfigured. -Preview the production build locally: ```bash -npm run preview +npm run preview # local prod preview ``` +### Docker (Netcup VPS) + +This repo is the deployable unit for `cc.cloud.librete.ch` on the +Netcup VPS — sibling to `caddy`, `immich`, `mp`, `umami` (see +`libretech/netcup`). Multi-stage `Dockerfile` builds the static bundle +and serves it via nginx; `compose.yaml` joins the external `edge` +network so Caddy reverse-proxies to it. + +```sh +# from a workstation +git push +ssh netcup +cd /srv/cc +git pull +docker compose build +docker compose up -d +``` + +#### First-time setup on the server + +```sh +ssh netcup +git clone ssh://tengo@git.librete.ch:41240/libretech/code-crispies.git /srv/cc +cd /srv/cc +cp .env.example .env +$EDITOR .env # fill VITE_SUPABASE_URL + VITE_SUPABASE_ANON_KEY +chmod 600 .env +docker compose build +docker compose up -d + +# Verify +docker compose ps +docker compose exec -T cc wget -qO- http://127.0.0.1/health +curl -sS https://cc.cloud.librete.ch/ # via caddy +``` + +The nginx config inside the image rewrites unknown paths to +`index.html` so client-side routing keeps working. `VITE_SUPABASE_*` +are baked into the bundle at `docker compose build`, so a rebuild is +needed when they change. + ## 🌐 Internationalization The project supports multiple languages: diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..1301491 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,23 @@ +name: cc + +services: + cc: + build: + context: . + args: + VITE_SUPABASE_URL: ${VITE_SUPABASE_URL} + VITE_SUPABASE_ANON_KEY: ${VITE_SUPABASE_ANON_KEY} + image: cc:local + restart: always + networks: + - edge + healthcheck: + test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1/health || exit 1'] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + +networks: + edge: + external: true