Single self-contained index.html (avatar + favicon inlined; only Google Fonts external; zero JS; dark-mode; responsive) served by a baked nginx:alpine image behind Traefik, built and published by CI on push to main and rolled out by Watchtower. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LbhPvfSERrnuY5jdhAdB7v
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
name: build
|
||||
|
||||
# On push to main, build the self-contained nginx image and push it to the
|
||||
# build registry, then publish it so the deploy host can pull it over HTTPS
|
||||
# and Watchtower recreates the container. The registry address is injected
|
||||
# from a masked secret so it never appears in the repo or the logs.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'index.html'
|
||||
- 'favicon.svg'
|
||||
- 'nginx.conf'
|
||||
- 'Dockerfile'
|
||||
- '.gitea/workflows/build.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# The build registry is plain HTTP — tell buildkit not to upgrade
|
||||
# the push to HTTPS.
|
||||
config-inline: |
|
||||
[registry."${{ secrets.REGISTRY_HOST }}"]
|
||||
http = true
|
||||
insecure = true
|
||||
|
||||
- name: Configure registry credentials for buildx
|
||||
env:
|
||||
REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }}
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
mkdir -p ~/.docker
|
||||
AUTH=$(printf '%s:%s' "$REGISTRY_USER" "$REGISTRY_TOKEN" | base64 -w0)
|
||||
cat > ~/.docker/config.json <<EOF
|
||||
{"auths":{"$REGISTRY_HOST":{"auth":"$AUTH"}}}
|
||||
EOF
|
||||
|
||||
- name: Compute tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.REGISTRY_HOST }}/justin/jpaulio
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=raw,value=test-main
|
||||
type=sha,prefix=test-sha-,format=long
|
||||
labels: |
|
||||
org.opencontainers.image.url=https://jpaul.io
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Publish package (link to repo)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
code=$(curl -s -o /tmp/link.out -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"https://git.jpaul.io/api/v1/packages/justin/container/jpaulio/-/link/jpaulio")
|
||||
echo "link -> jpaulio: HTTP $code"
|
||||
case "$code" in
|
||||
201) echo "OK — newly linked" ;;
|
||||
400|409) echo "OK — already linked" ;;
|
||||
*) cat /tmp/link.out; exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,7 @@
|
||||
# local scratch — the avatar is inlined into index.html as a data URI, so these
|
||||
# downloaded source files aren't needed in the repo
|
||||
.assets/
|
||||
|
||||
# Claude Code local files (skill config with internal deploy notes, local
|
||||
# settings) stay out of the repo
|
||||
.claude/
|
||||
@@ -0,0 +1,7 @@
|
||||
# Self-contained image for the jpaul.io hub page: nginx:alpine with the site
|
||||
# and config baked in. No bind mounts, no inode surprises — the running
|
||||
# container always matches the image it was built from.
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY index.html favicon.svg /usr/share/nginx/html/
|
||||
@@ -0,0 +1,33 @@
|
||||
# jpaul.io
|
||||
|
||||
The personal hub landing page at the root of [jpaul.io](https://jpaul.io) — who I am,
|
||||
what I make, and where to find me. Not a blog, not a portfolio: a hub.
|
||||
|
||||
## Contents
|
||||
|
||||
- **`index.html`** — the whole site. One self-contained file, no build step. The avatar
|
||||
and favicon are inlined as data URIs; the only external assets are the Google Fonts
|
||||
(Inter + JetBrains Mono). Zero JavaScript.
|
||||
- **`favicon.svg`** — green `$` / amber `_` cursor mark on the GitHub-dark-slate ground.
|
||||
- **`Dockerfile`** — bakes the site into an `nginx:alpine` image.
|
||||
- **`docker-compose.yml`** — runs that image behind Traefik.
|
||||
- **`.gitea/workflows/build.yml`** — CI: build the image and publish it on push to `main`.
|
||||
|
||||
## Design
|
||||
|
||||
Dark-mode only, GitHub-dark-slate palette, terminal-panel motifs. Matches the brand used
|
||||
across [jpaul.me](https://www.jpaul.me), the AI Workflow course cards, and the YouTube
|
||||
channel. Responsive; cards stack to one column below 720px; hero stays readable at 375px.
|
||||
|
||||
## Deploy — automated (push to main → live)
|
||||
|
||||
1. **Push to `main`** → Gitea Actions builds the `nginx:alpine` image and publishes it.
|
||||
2. The deploy host **pulls** the image over HTTPS.
|
||||
3. **Watchtower** recreates the container with the new image.
|
||||
|
||||
No bind mounts — the running container always matches what CI built. Runs behind Traefik:
|
||||
`websecure` entrypoint, an HTTP-01 cert, and `Host(jpaul.io) || Host(www.jpaul.io)` with
|
||||
the cert domains named explicitly (a compound rule won't auto-populate them).
|
||||
|
||||
First run on the host: `docker compose up -d`. After that, editing the site is just
|
||||
commit → push to `main`; CI and Watchtower do the rest.
|
||||
@@ -0,0 +1,31 @@
|
||||
# jpaul.io hub page — a single container behind Traefik. The site is baked into
|
||||
# the image by CI (see .gitea/workflows/build.yml + Dockerfile); Watchtower
|
||||
# recreates the container when a new image is published, so what's running
|
||||
# always matches what CI built.
|
||||
#
|
||||
# First run: docker compose up -d
|
||||
# Updates: automatic — push to main -> CI builds -> Watchtower redeploys.
|
||||
services:
|
||||
jpaulio:
|
||||
image: git.jpaul.io/justin/jpaulio:latest
|
||||
container_name: jpaulio
|
||||
restart: always
|
||||
pull_policy: always
|
||||
networks:
|
||||
- web
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.jpaulio.rule=Host(`jpaul.io`) || Host(`www.jpaul.io`)"
|
||||
- "traefik.http.routers.jpaulio.entrypoints=websecure"
|
||||
- "traefik.http.routers.jpaulio.tls.certresolver=myresolver"
|
||||
# A compound (||) rule doesn't auto-populate the ACME domain list, so name
|
||||
# the cert domains explicitly: one cert for the apex + www SAN.
|
||||
- "traefik.http.routers.jpaulio.tls.domains[0].main=jpaul.io"
|
||||
- "traefik.http.routers.jpaulio.tls.domains[0].sans=www.jpaul.io"
|
||||
- "traefik.http.services.jpaulio.loadbalancer.server.port=80"
|
||||
# Watchtower auto-updates this container when a new image is published.
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="jpaul.io">
|
||||
<rect width="64" height="64" rx="14" fill="#0D1117"/>
|
||||
<rect x="1.75" y="1.75" width="60.5" height="60.5" rx="12.25" fill="none" stroke="#30363D" stroke-width="1.5"/>
|
||||
<text x="25" y="45" font-family="'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace" font-size="42" font-weight="700" fill="#00D966" text-anchor="middle">$</text>
|
||||
<text x="41" y="45" font-family="'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace" font-size="42" font-weight="700" fill="#FFB020" text-anchor="middle">_</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 633 B |
+527
File diff suppressed because one or more lines are too long
+23
@@ -0,0 +1,23 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Compress the (mostly text) payload. The inlined base64 avatar doesn't
|
||||
# shrink much, but the HTML/SVG/CSS around it does.
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 256;
|
||||
gzip_types text/html text/css application/javascript image/svg+xml application/json application/xml;
|
||||
|
||||
# It's a two-file site. Revalidate so copy edits (git pull, no restart)
|
||||
# go live on the next refresh instead of sticking in a cache.
|
||||
add_header Cache-Control "no-cache" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user