diff options
| -rw-r--r-- | assets/public_key | 2 | ||||
| -rwxr-xr-x | deploy-docker.sh | 26 | ||||
| -rwxr-xr-x | deploy.sh | 29 | ||||
| -rw-r--r-- | nginx.conf | 43 |
4 files changed, 78 insertions, 22 deletions
diff --git a/assets/public_key b/assets/public_key index dd5ffc4..9287723 100644 --- a/assets/public_key +++ b/assets/public_key @@ -1 +1 @@ -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJV3IQljIJbI0Uz5MN77zChMZNkMastBtoUp8RsHa3br +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJV3IQljIJbI0Uz5MN77zChMZNkMastBtoUp8RsHa3br diff --git a/deploy-docker.sh b/deploy-docker.sh index 2e72954..716bb8d 100755 --- a/deploy-docker.sh +++ b/deploy-docker.sh @@ -57,14 +57,32 @@ cp cv/index.txt public/cv.txt cp -r assets/* public/ cp -r assets/* public/assets/ -# nginx maps curl/wget UAs to .txt, so `curl https://jayrup.me/public_key` -# resolves to /public_key.txt. Single source of truth stays assets/public_key; -# browsers keep getting the suffixless /public_key. -cp assets/public_key public/public_key.txt +# .txt twin for curl/wget: normalize to exactly one line ending in a newline, +# so `curl https://jayrup.me/public_key >> ~/.ssh/authorized_keys` appends a +# clean key instead of gluing itself onto the previous line. +KEY="$(tr -d '\r' < assets/public_key | sed -e 's/[[:space:]]*$//' | tr -d '\n')" +[ -n "$KEY" ] || { echo "assets/public_key is empty" >&2; exit 1; } +printf '%s\n' "$KEY" > public/public_key.txt # Deploy status marker — site-visible proof of the last successful deploy printf 'deployed %s commit %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SHORT_SHA" > public/last-deploy.txt echo ">> pushing to $DEPLOY_HOST" rsync -avz --delete public/ "$DEPLOY_HOST":~/homepage/public/ + +# nginx config is bind-mounted into the container, so it does not travel with +# public/. Test before reloading: a bad file must not take down the running +# workers, so restore the previous one and fail loudly instead. +echo ">> pushing nginx.conf" +ssh "$DEPLOY_HOST" 'cp -a ~/homepage/nginx.conf ~/homepage/nginx.conf.prev' +rsync -avz nginx.conf "$DEPLOY_HOST":~/homepage/nginx.conf +if ssh "$DEPLOY_HOST" 'docker exec homepage nginx -t'; then + ssh "$DEPLOY_HOST" 'docker exec homepage nginx -s reload' + echo ">> nginx reloaded" +else + echo "!! nginx config failed its test - restoring previous config" >&2 + ssh "$DEPLOY_HOST" 'mv ~/homepage/nginx.conf.prev ~/homepage/nginx.conf' + exit 1 +fi + echo ">> deploy OK ($SHORT_SHA)" @@ -11,6 +11,7 @@ set -euo pipefail # call must be pointed at $REPO explicitly. REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SHA="$(git -C "$REPO" rev-parse --short HEAD)" +DEPLOY_HOST="${DEPLOY_HOST:-jayrup.me}" # 1. Render into a fresh temp dir so a partial build never gets deployed BUILD_DIR="$(mktemp -d /tmp/homepage-local.XXXXXX)" @@ -45,14 +46,32 @@ mkdir -p public/assets cp -r assets/* public/ cp -r assets/* public/assets/ -# .txt twin: curl/wget UAs map to .txt, so `curl https://jayrup.me/public_key` -# resolves to public_key.txt. Source of truth stays assets/public_key. -cp assets/public_key public/public_key.txt +# .txt twin for curl/wget: normalize to exactly one line ending in a newline, +# so `curl https://jayrup.me/public_key >> ~/.ssh/authorized_keys` appends a +# clean key instead of gluing itself onto the previous line. +KEY="$(tr -d '\r' < assets/public_key | sed -e 's/[[:space:]]*$//' | tr -d '\n')" +[ -n "$KEY" ] || { echo "assets/public_key is empty" >&2; exit 1; } +printf '%s\n' "$KEY" > public/public_key.txt # 6. Deploy marker printf 'deployed %s commit %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SHA" > public/last-deploy.txt echo "Pushing to vps" -# Only reached if all renders succeeded (set -e) -rsync -avz --delete public/ jayrup.me:~/homepage/public/ +# 7. Content. Only reached if all renders succeeded (set -e) +rsync -avz --delete public/ "$DEPLOY_HOST":~/homepage/public/ + +# 8. nginx config. It is bind-mounted into the container, so it does not travel +# with public/. Test it before reloading: a bad file must not take down the +# running workers, so restore the previous one and fail loudly instead. +echo "Pushing nginx.conf" +ssh "$DEPLOY_HOST" 'cp -a ~/homepage/nginx.conf ~/homepage/nginx.conf.prev' +rsync -avz nginx.conf "$DEPLOY_HOST":~/homepage/nginx.conf +if ssh "$DEPLOY_HOST" 'docker exec homepage nginx -t'; then + ssh "$DEPLOY_HOST" 'docker exec homepage nginx -s reload' + echo "nginx reloaded" +else + echo "!! nginx config failed its test - restoring previous config" >&2 + ssh "$DEPLOY_HOST" 'mv ~/homepage/nginx.conf.prev ~/homepage/nginx.conf' + exit 1 +fi @@ -17,6 +17,12 @@ server { # Don't leak nginx version on error pages / server header server_tokens off; + # The whole site is UTF-8. Without this, text/plain responses (i.e. what + # curl gets) carry no charset at all, so em dashes and other non-ASCII + # bytes are left ambiguous. + charset utf-8; + charset_types text/css text/plain text/xml application/javascript application/json application/rss+xml; + # Gzip compression gzip on; gzip_vary on; @@ -25,28 +31,42 @@ server { gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss image/svg+xml; # Security headers + # + # add_header does NOT merge across levels: a location that declares any + # add_header inherits none from here. Every location below that adds a + # header therefore repeats these three verbatim. add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; - # Static asset caching + # Static asset caching. + # Cache-Control is set explicitly rather than via `expires`, because + # `expires` emits its own Cache-Control and the pair would duplicate. + # No `always`: a cached 404 for a missing asset is worse than no cache. location ~* \.(ico|css|js|gif|jpe?g|png|svg|woff2?)$ { - expires 30d; - add_header Cache-Control "public, no-transform"; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Cache-Control "public, max-age=2592000, no-transform"; + try_files $uri =404; } - # Compatibility for assets prefix + # Compatibility for the assets prefix. These must honour $final_suffix: + # the deploy script ships the key as both /public_key and /public_key.txt, + # and only the .txt twin exists for curl/wget. location = /assets/public_key { - try_files /public_key /assets/public_key =404; + try_files /public_key$final_suffix /public_key /assets/public_key =404; } location = /assets/dissertation.pdf { try_files /dissertation.pdf /assets/dissertation.pdf =404; } - # Serve PDFs directly + # Serve PDFs inline. `default_type` sets Content-Type; using add_header + # here would emit a second Content-Type next to the one from mime.types. + # Content-Disposition is omitted deliberately — nginx already serves + # unknown-to-it types inline, and browsers render PDFs inline by default. location ~* \.pdf$ { - add_header Content-Type application/pdf; - add_header Content-Disposition inline; + default_type application/pdf; try_files $uri =404; } @@ -78,8 +98,7 @@ server { =404; } - location ~* \.txt$ { - add_header Content-Type text/plain; - } + # No explicit .txt location: mime.types already maps .txt to text/plain and + # `charset utf-8` above appends the charset. Declaring one only to set + # Content-Type would drop the inherited security headers. } - |
