Matches the vfxreview/xbid setup: runner pulls and runs docker compose; nginx:alpine serves the static export on the npm_default network behind Nginx Proxy Manager. DEPLOY.md has the one-time SSH setup steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
.git
|
||||||
|
.claude
|
||||||
|
content/*.mp4
|
||||||
|
content/*.pdf
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
volumes:
|
||||||
|
- /opt:/opt
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
cd /opt/ttvfx_web
|
||||||
|
|
||||||
|
git pull
|
||||||
|
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
docker image prune -f
|
||||||
@@ -1,86 +1,139 @@
|
|||||||
# Deploying twotalesvfx.com
|
# Deploying twotalesvfx.com
|
||||||
|
|
||||||
The site is a fully static export, so there is no Node process to run in production.
|
Same setup as the other apps (vfxreview, xbid):
|
||||||
|
|
||||||
## 1. Build
|
```
|
||||||
|
git push main ──► Gitea Actions runner ──► cd /opt/ttvfx_web && git pull
|
||||||
|
docker compose up -d --build
|
||||||
|
│
|
||||||
|
Cloudflare ──► Nginx Proxy Manager ──► ttvfx-web:80 (nginx, static files)
|
||||||
|
(npm_default network)
|
||||||
|
```
|
||||||
|
|
||||||
|
Files involved:
|
||||||
|
|
||||||
|
| File | What it does |
|
||||||
|
|------|--------------|
|
||||||
|
| `Dockerfile` | Builds the static export with Node 20, then serves `out/` from `nginx:alpine` |
|
||||||
|
| `docker/nginx.conf` | nginx config inside the container (routes, 404 page, caching, relative redirects) |
|
||||||
|
| `docker-compose.yml` | One service, container name `ttvfx-web`, joined to the external `npm_default` network, no host ports |
|
||||||
|
| `.gitea/workflows/deploy.yml` | On push to `main`: pull in `/opt/ttvfx_web` and rebuild |
|
||||||
|
|
||||||
|
There is no `.env` and no database. The site is fully static.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## One-time setup on the server
|
||||||
|
|
||||||
|
### 1. SSH in
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
ssh <you>@178.104.56.62 # git.twotalesvfx.com's IP; use your usual host if different
|
||||||
npm run build # outputs ./out
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Or build on the server straight from git:
|
### 2. Check how the existing apps are set up, and copy that
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.twotalesvfx.com/pinto/ttvfx.git /opt/ttvfx
|
ls -la /opt # note the owner of /opt/xbid and /opt/vfxreview
|
||||||
cd /opt/ttvfx && npm ci && npm run build
|
git -C /opt/xbid remote -v # note HOW it authenticates (https+token, ssh, ...)
|
||||||
|
docker network ls | grep npm_default
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. DNS (Cloudflare)
|
`npm_default` must exist. It's the Nginx Proxy Manager network.
|
||||||
|
|
||||||
`twotalesvfx.com` is already on Cloudflare (proxied). Point the apex and `www`
|
### 3. Clone into /opt/ttvfx_web
|
||||||
at the VPS and leave the existing `review` and `git` records alone:
|
|
||||||
|
|
||||||
| Type | Name | Value | Proxy |
|
Use the same remote style as `/opt/xbid` so the runner's `git pull` can authenticate.
|
||||||
|------|-------|--------------|---------|
|
For example, if xbid uses an https remote:
|
||||||
| A | `@` | `<VPS IPv4>` | Proxied |
|
|
||||||
| A | `www` | `<VPS IPv4>` | Proxied |
|
|
||||||
|
|
||||||
At the time of writing the apex returned Cloudflare **522** (origin unreachable),
|
|
||||||
so the current `@` record needs updating.
|
|
||||||
|
|
||||||
SSL/TLS mode: **Full (strict)**, using either certbot (below) or a Cloudflare
|
|
||||||
Origin Certificate installed in nginx.
|
|
||||||
|
|
||||||
## 3. Upload
|
|
||||||
|
|
||||||
If building locally:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rsync -avz --delete out/ user@your-vps:/var/www/twotalesvfx.com/
|
sudo git clone https://git.twotalesvfx.com/pinto/ttvfx.git /opt/ttvfx_web
|
||||||
```
|
```
|
||||||
|
|
||||||
If building on the server, copy or symlink `/opt/ttvfx/out` to `/var/www/twotalesvfx.com`.
|
Then match the ownership of the other app folders (replace `<owner>` with what `ls -la /opt` showed):
|
||||||
|
|
||||||
## 4. nginx
|
|
||||||
|
|
||||||
`/etc/nginx/sites-available/twotalesvfx.com`:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name twotalesvfx.com www.twotalesvfx.com;
|
|
||||||
root /var/www/twotalesvfx.com;
|
|
||||||
|
|
||||||
# www -> apex
|
|
||||||
if ($host = www.twotalesvfx.com) { return 301 https://twotalesvfx.com$request_uri; }
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ $uri.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Hashed build assets can be cached forever
|
|
||||||
location /_next/static/ {
|
|
||||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Video: allow range requests to be cached
|
|
||||||
location ~* \.mp4$ {
|
|
||||||
add_header Cache-Control "public, max-age=604800";
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 404 /404.html;
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_types text/css application/javascript image/svg+xml application/json;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ln -s /etc/nginx/sites-available/twotalesvfx.com /etc/nginx/sites-enabled/
|
sudo chown -R <owner>:<owner> /opt/ttvfx_web
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
|
||||||
sudo certbot --nginx -d twotalesvfx.com -d www.twotalesvfx.com # HTTPS via Let's Encrypt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
(For Apache, set `DocumentRoot` to the same folder plus `ErrorDocument 404 /404.html`.
|
If xbid's remote URL contains a token or uses SSH, clone this one the same way
|
||||||
Because the build uses `trailingSlash`, every page is a folder with its own `index.html`.)
|
(e.g. `git clone git@git.twotalesvfx.com:pinto/ttvfx.git /opt/ttvfx_web`).
|
||||||
|
|
||||||
|
### 4. First build by hand
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/ttvfx_web
|
||||||
|
docker compose up -d --build
|
||||||
|
docker ps --filter name=ttvfx-web # should be "Up"
|
||||||
|
```
|
||||||
|
|
||||||
|
The first build takes a minute or two (npm install + Next build). Later builds reuse the cache.
|
||||||
|
|
||||||
|
Check that it serves from inside the proxy network:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --network npm_default curlimages/curl -sI http://ttvfx-web/
|
||||||
|
# -> HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Nginx Proxy Manager: add the proxy host
|
||||||
|
|
||||||
|
In the NPM web UI → **Hosts → Proxy Hosts → Add Proxy Host**:
|
||||||
|
|
||||||
|
- **Domain names:** `twotalesvfx.com`, `www.twotalesvfx.com`
|
||||||
|
- **Scheme:** `http`
|
||||||
|
- **Forward hostname / IP:** `ttvfx-web`
|
||||||
|
- **Forward port:** `80`
|
||||||
|
- Tick **Block common exploits**. Websockets aren't needed.
|
||||||
|
- **SSL tab:** request a new Let's Encrypt certificate, then tick **Force SSL** and **HTTP/2**.
|
||||||
|
- Cloudflare is proxying the domain. If the HTTP challenge fails, either grey-cloud the two
|
||||||
|
records for a minute while the cert is issued, or use **DNS challenge → Cloudflare**
|
||||||
|
with an API token. Use whichever you did for `review.` / `xbid.`.
|
||||||
|
|
||||||
|
### 6. Cloudflare DNS
|
||||||
|
|
||||||
|
The apex currently returns **522** (Cloudflare can't reach the origin). Point it at this server:
|
||||||
|
|
||||||
|
| Type | Name | Value | Proxy |
|
||||||
|
|------|-------|------------------|---------|
|
||||||
|
| A | `@` | `178.104.56.62` | Proxied |
|
||||||
|
| A | `www` | `178.104.56.62` | Proxied |
|
||||||
|
|
||||||
|
(Confirm the server's IP with `curl -4 ifconfig.me` on the box.) Leave `review`, `git`, `xbid` etc. alone.
|
||||||
|
Set **SSL/TLS → Full (strict)**, the same as the other subdomains.
|
||||||
|
|
||||||
|
### 7. Turn on auto-deploy
|
||||||
|
|
||||||
|
The workflow is already in the repo (`.gitea/workflows/deploy.yml`). In Gitea, open
|
||||||
|
**pinto/ttvfx → Settings → Repository → Enable Actions** if it isn't enabled already
|
||||||
|
(it's on for the other repos). The existing runner picks the job up. It uses the same
|
||||||
|
`ubuntu-latest` label and `/opt:/opt` mount as xbid.
|
||||||
|
|
||||||
|
Test it: push any commit to `main` and watch **Actions** in the repo. The job should run
|
||||||
|
`git pull` and `docker compose up -d --build`, then the site updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Day to day
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push # from your machine. That's it, the runner deploys.
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual redeploy or logs on the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/ttvfx_web && git pull && docker compose up -d --build
|
||||||
|
docker logs -f ttvfx-web
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Fix |
|
||||||
|
|---------|-----|
|
||||||
|
| Action fails on `git pull` with *dubious ownership* | `git config --global --add safe.directory /opt/ttvfx_web` (run it however you did for xbid), or fix ownership to match `/opt/xbid` |
|
||||||
|
| Action fails on `git pull` with *authentication* | The clone's remote URL needs the same credentials as `/opt/xbid` (`git -C /opt/xbid remote -v`) |
|
||||||
|
| `network npm_default declared as external, but could not be found` | NPM's network has a different name on this box. Check `docker network ls` and update `docker-compose.yml` |
|
||||||
|
| NPM shows **502 Bad Gateway** | Container not running (`docker ps`) or wrong forward host. It must be `ttvfx-web`, port `80` |
|
||||||
|
| Cloudflare **522** | DNS `@`/`www` still point at the wrong IP, or ports 80/443 are firewalled |
|
||||||
|
| Old content after deploy | Purge Cloudflare cache (Caching → Purge Everything). HTML isn't cached long, but video and images are cached for 7 days |
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# ---- Builder: static export -> /app/out ----
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---- Runner: plain nginx serving the static files ----
|
||||||
|
FROM nginx:1.27-alpine AS runner
|
||||||
|
|
||||||
|
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=builder /app/out /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: ttvfx-web
|
||||||
|
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- npm_default
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm_default:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Serves the Next.js static export. TLS and the public hostname are handled
|
||||||
|
# by Nginx Proxy Manager in front of this container.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
# Behind a TLS proxy: keep redirects relative so they stay on https
|
||||||
|
absolute_redirect off;
|
||||||
|
|
||||||
|
# Pages are exported as folder/index.html (trailingSlash: true)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ $uri.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Content-hashed build assets never change
|
||||||
|
location /_next/static/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Video and images: cache for a week (nginx handles range requests)
|
||||||
|
location ~* \.(mp4|jpg|jpeg|png|webp|svg)$ {
|
||||||
|
add_header Cache-Control "public, max-age=604800";
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript image/svg+xml application/json text/plain application/xml;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user