Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# API build context is the repo root (compose: context ..). Keep it lean so the
# `COPY . .` in src/Cluckwork.Api/Dockerfile doesn't pull build junk.
**/bin
**/obj
**/.vs
web/node_modules
web/dist
web/.vite
**/*.tsbuildinfo
.git
.github
**/.env
!**/.env.example
deploy/.env
26 changes: 22 additions & 4 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,29 @@ services:
db:
condition: service_healthy
labels:
# API owns /api on the same host; higher priority than the web catch-all.
- traefik.enable=true
- traefik.http.routers.cluckwork.rule=Host(`${CLUCKWORK_HOST}`)
- traefik.http.routers.cluckwork.entrypoints=websecure
- traefik.http.routers.cluckwork.tls=true
- traefik.http.services.cluckwork.loadbalancer.server.port=8080
- traefik.http.routers.cluckwork-api.rule=Host(`${CLUCKWORK_HOST}`) && PathPrefix(`/api`)
- traefik.http.routers.cluckwork-api.priority=10
- traefik.http.routers.cluckwork-api.entrypoints=websecure
- traefik.http.routers.cluckwork-api.tls=true
- traefik.http.services.cluckwork-api.loadbalancer.server.port=8080

web:
build:
context: ../web
dockerfile: Dockerfile
depends_on:
- api
labels:
# Serves the SPA for everything on the host that is not /api. The SPA calls
# relative /api/v1/... which traefik routes to the api service above.
- traefik.enable=true
- traefik.http.routers.cluckwork-web.rule=Host(`${CLUCKWORK_HOST}`)
- traefik.http.routers.cluckwork-web.priority=1
- traefik.http.routers.cluckwork-web.entrypoints=websecure
- traefik.http.routers.cluckwork-web.tls=true
- traefik.http.services.cluckwork-web.loadbalancer.server.port=80

db:
image: postgres:16-alpine
Expand Down
10 changes: 10 additions & 0 deletions web/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules
dist
dist-ssr
.vite
*.tsbuildinfo
.env
.env.*
!.env.example
.git
README.md
14 changes: 14 additions & 0 deletions web/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# --- Build stage: compile the SPA to static assets ---
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# --- Runtime stage: serve static assets via nginx ---
FROM nginx:1.27-alpine AS final
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
# nginx:alpine already runs `nginx -g "daemon off;"` as its default CMD.
28 changes: 28 additions & 0 deletions web/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;

# gzip static assets
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 256;

# Hashed build assets are immutable — cache hard.
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}

# SPA client-side routing: unknown paths fall back to index.html.
location / {
try_files $uri $uri/ /index.html;
}

# index.html itself must never be cached, so new deploys are picked up.
location = /index.html {
add_header Cache-Control "no-cache";
}
}
Loading