feat(deploy): docker compose + nginx static for netcup VPS

This commit is contained in:
2026-04-28 18:58:32 +02:00
parent 45fc2a43f0
commit d0dbd3c257
5 changed files with 133 additions and 5 deletions

15
.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
node_modules
dist
coverage
.env
.env.*
!.env.example
.git
.github
.idea
.vscode
.direnv
.wave
.claude
.navi
*.log

5
.env.example Normal file
View File

@@ -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://<your-project>.supabase.co
VITE_SUPABASE_ANON_KEY=<anon-key>

45
Dockerfile Normal file
View File

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

View File

@@ -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:

23
compose.yaml Normal file
View File

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