From c877c61e3e911e69d7d46573f22e230fb01e590e Mon Sep 17 00:00:00 2001 From: cesnimda Date: Fri, 10 Jul 2026 10:00:33 +0200 Subject: [PATCH] deploy: publish site on host port 1337; nginx proxies /api/contact; auto-deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - site nginx listens on 1337; proxies /api/contact to the relay over the internal network (single public port; TLS terminates upstream at Cloudflare) - trailing-slash rewrite moved inside location / so /api/contact isn't redirected - compose: publish ${SITE_PORT:-1337}, internal bridge network, no Traefik labels (non-destructive — does not touch the WordPress apex router) - .gitea/workflows/deploy.yml: on push to main, runner rebuilds + restarts the stack Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/deploy.yml | 16 ++++++++++++++ deploy/.env.example | 7 +++--- deploy/docker-compose.yml | 44 +++++++++++++------------------------ site/Dockerfile | 2 +- site/nginx.conf | 17 +++++++++----- 5 files changed, 47 insertions(+), 39 deletions(-) create mode 100644 .gitea/workflows/deploy.yml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..ac13e76 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,16 @@ +name: Deploy + +# Auto-deploy on push to main. Runs on the self-hosted Gitea runner (which has the +# host Docker socket) and rebuilds + restarts the stack on the same host. +# The public site ends up on host port 1337 (see deploy/docker-compose.yml). +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build and (re)start the stack + run: docker compose -f deploy/docker-compose.yml up -d --build --remove-orphans diff --git a/deploy/.env.example b/deploy/.env.example index a07c79f..454cd2d 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -14,7 +14,6 @@ RELAY_WINDOW_SECONDS=600 # --- Site build --- PUBLIC_SITE_URL=https://cesnimda.co.uk -# --- Infra (Traefik host reverse proxy) --- -PROXY_NETWORK=traefik_proxy # existing external Traefik docker network -SITE_HOST=cesnimda.co.uk # Host rule for the Traefik routers -TRAEFIK_ENTRYPOINT=websecure-external # Traefik entrypoint name on the host +# --- Infra --- +SITE_PORT=1337 # host port the public-facing site is published on + # (put Cloudflare / your TLS proxy in front of it) diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 84ff6d3..1400480 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,8 +1,6 @@ -# Production stack (DOCKER_SPEC §2). Publishes no host ports — Traefik (the host's -# existing reverse proxy) discovers these containers on the shared traefik_proxy -# network via the labels below and routes cesnimda.co.uk to the site, and -# cesnimda.co.uk/api/contact to the relay. Entrypoint / cert-resolver names match -# the host Traefik convention; override via the .env values if they differ. +# Production stack. The public-facing site is published on host port 1337 (put your +# TLS terminator / Cloudflare in front of it). nginx inside the site container proxies +# /api/contact to the relay over the internal network, so only one port is exposed. services: site: @@ -16,14 +14,11 @@ services: - /tmp - /var/cache/nginx - /var/run - networks: [proxy] - labels: - - traefik.enable=true - - traefik.docker.network=${PROXY_NETWORK:-traefik_proxy} - - traefik.http.routers.resumesite.rule=Host(`${SITE_HOST:-cesnimda.co.uk}`) - - traefik.http.routers.resumesite.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure-external} - - traefik.http.routers.resumesite.tls=true - - traefik.http.services.resumesite.loadbalancer.server.port=8080 + ports: + - '${SITE_PORT:-1337}:1337' + depends_on: + - relay + networks: [web] logging: driver: json-file options: { max-size: '10m', max-file: '3' } @@ -36,29 +31,20 @@ services: restart: unless-stopped read_only: true environment: - - Smtp__Host=${SMTP_HOST} + - Smtp__Host=${SMTP_HOST:-} - Smtp__Port=${SMTP_PORT:-587} - - Smtp__User=${SMTP_USER} - - Smtp__Password=${SMTP_PASSWORD} + - Smtp__User=${SMTP_USER:-} + - Smtp__Password=${SMTP_PASSWORD:-} - Relay__FromAddress=${RELAY_FROM:-} - - Relay__ToAddress=${RELAY_TO} + - Relay__ToAddress=${RELAY_TO:-connor.babbington@cesnimda.co.uk} - Relay__AllowedOrigin=${RELAY_ALLOWED_ORIGIN:-https://cesnimda.co.uk} - Relay__RateLimitPerWindow=${RELAY_RATE_LIMIT:-5} - Relay__WindowSeconds=${RELAY_WINDOW_SECONDS:-600} - networks: [proxy] - labels: - - traefik.enable=true - - traefik.docker.network=${PROXY_NETWORK:-traefik_proxy} - # More specific rule than the site router, so /api/contact wins. - - traefik.http.routers.resumerelay.rule=Host(`${SITE_HOST:-cesnimda.co.uk}`) && PathPrefix(`/api/contact`) - - traefik.http.routers.resumerelay.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure-external} - - traefik.http.routers.resumerelay.tls=true - - traefik.http.services.resumerelay.loadbalancer.server.port=8081 + networks: [web] logging: driver: json-file options: { max-size: '10m', max-file: '3' } networks: - proxy: - external: true - name: ${PROXY_NETWORK:-traefik_proxy} + web: + driver: bridge diff --git a/site/Dockerfile b/site/Dockerfile index 8dafb1c..929177e 100644 --- a/site/Dockerfile +++ b/site/Dockerfile @@ -14,5 +14,5 @@ RUN pnpm build FROM nginxinc/nginx-unprivileged:1.27-alpine AS final COPY --chown=nginx:nginx nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build --chown=nginx:nginx /app/dist /usr/share/nginx/html -EXPOSE 8080 +EXPOSE 1337 # runs as uid 101 (nginx) by default in this image diff --git a/site/nginx.conf b/site/nginx.conf index 1009f75..f987c0d 100644 --- a/site/nginx.conf +++ b/site/nginx.conf @@ -3,7 +3,7 @@ # trailing slashes, the preserved /Linkedin redirect, and legacy-WP 410s. server { - listen 8080; + listen 1337; server_name _; root /usr/share/nginx/html; index index.html; @@ -32,6 +32,14 @@ server { return 301 https://www.linkedin.com/in/connor-babbington; } + # --- Contact relay (same-origin; single public port) --- + location = /api/contact { + proxy_pass http://relay:8081; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + # --- Legacy WordPress URLs -> 410 Gone (crawler cleanup) --- location ~* ^/(wp-admin|wp-login|wp-content|wp-includes|wp-json|xmlrpc\.php|feed|comments/feed) { return 410; @@ -59,11 +67,10 @@ server { add_header Cache-Control "public, must-revalidate" always; } - # --- Canonical trailing slash for extensionless paths (SEO) --- - rewrite ^([^.]*[^/])$ $1/ permanent; - - # --- HTML documents: revalidate so deploys are instant (inherits headers above) --- + # --- HTML documents: canonical trailing slash + revalidate. Kept inside the + # catch-all so exact routes (e.g. /api/contact) are never slash-redirected. --- location / { + rewrite ^([^.]*[^/])$ $1/ permanent; try_files $uri $uri/ =404; }