From 55e585186eb80b43a591180c8640f411e0479e9a Mon Sep 17 00:00:00 2001 From: Matthew L McPeak Date: Wed, 10 Jun 2026 17:03:17 -0400 Subject: [PATCH] feat: docker --- .dockerignore | 8 ++ .forgejo/workflows/docker-publish.yml | 67 +++++++++++++++ Dockerfile | 7 ++ README.md | 25 ++++++ nginx.conf | 10 +++ src/data/portfolio.ts | 112 +++++++++++++------------- 6 files changed, 174 insertions(+), 55 deletions(-) create mode 100644 .dockerignore create mode 100644 .forgejo/workflows/docker-publish.yml create mode 100644 Dockerfile create mode 100644 nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..24447fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +dist-ssr +coverage +test-results +playwright-report +.git +.vscode +*.log diff --git a/.forgejo/workflows/docker-publish.yml b/.forgejo/workflows/docker-publish.yml new file mode 100644 index 0000000..b610f2a --- /dev/null +++ b/.forgejo/workflows/docker-publish.yml @@ -0,0 +1,67 @@ +name: Build and publish Docker image + +on: + push: + tags: + - 'v*' + pull_request: + +jobs: + build: + runs-on: docker + container: oven/bun:1 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Audit dependencies + run: bun audit + + - name: Run unit tests + run: bun run test:unit run + + - name: Build + run: bun run build + + - name: Upload dist + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist + + docker: + needs: build + if: startsWith(github.ref, 'refs/tags/v') + runs-on: docker + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download dist + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Log in to registry + uses: docker/login-action@v3 + with: + registry: git.mcpeakdev.com + username: ${{ github.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Extract image tag + id: meta + run: echo "tag=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + git.mcpeakdev.com/mcpeakml/portfolio:${{ steps.meta.outputs.tag }} + git.mcpeakdev.com/mcpeakml/portfolio:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0cc02c4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +# syntax=docker/dockerfile:1 + +FROM nginx:1-alpine +COPY dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/README.md b/README.md index 0d4331e..42c44ac 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,28 @@ bun test:e2e --debug ```sh bun lint ``` + +## Docker + +The Dockerfile serves a pre-built `dist/` directory via nginx, so build the +app first: + +```sh +bun run build +docker build -t portfolio . +docker run --rm -p 8080:80 portfolio +``` + +The site will be available at http://localhost:8080. + +### CI publishing + +A Forgejo Actions workflow (`.forgejo/workflows/docker-publish.yml`) builds the +image and pushes it to the Forgejo container registry whenever a `v*` tag is +pushed (e.g. `v1.2.3`): + +- `git.mcpeakdev.com/mcpeakml/portfolio:` +- `git.mcpeakdev.com/mcpeakml/portfolio:latest` + +This requires a `REGISTRY_TOKEN` repository secret containing a token with +`write:package` permission for the registry. diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..3c25a98 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/src/data/portfolio.ts b/src/data/portfolio.ts index d9aa597..2b1a484 100644 --- a/src/data/portfolio.ts +++ b/src/data/portfolio.ts @@ -1,83 +1,85 @@ export interface Stat { - value: string - label: string - color: string + value: string; + label: string; + color: string; } export interface Project { - id: string - title: string - color: 'primary' | 'info' | 'success' | 'warn' - description: string - tags: string[] - link: string + id: string; + title: string; + color: "primary" | "info" | "success" | "warn"; + description: string; + tags: string[]; + link: string; } export interface PortfolioData { - name: string - role: string - tagline: string - status: string - about: string - stats: Stat[] - projects: Project[] - skills: Record + name: string; + role: string; + tagline: string; + status: string; + about: string; + stats: Stat[]; + projects: Project[]; + skills: Record; social: { - github: string - linkedin: string - email: string - } + github: string; + linkedin: string; + email: string; + }; } // Edit this block to make the portfolio yours. export const portfolioData: PortfolioData = { - name: 'Alexander Voss', - role: 'Product Designer · Developer', - tagline: 'I craft digital experiences that feel inevitable — clear, classical, enduring.', - status: 'Open for opportunities · 2026', + name: "Matthew McPeak", + role: "Product Designer · Developer", + tagline: "I craft digital experiences that feel inevitable — clear, classical, enduring.", + status: "Open for opportunities · 2026", about: - 'Based in Athens. I work at the intersection of design and engineering, building systems and products that are precise, durable, and a pleasure to use. Ten years in, I still believe the best interface is the one that gets out of the way.', + "Based in Athens. I work at the intersection of design and engineering, building systems and products that are precise, durable, and a pleasure to use. Ten years in, I still believe the best interface is the one that gets out of the way.", stats: [ - { value: '10+', label: 'Years of craft', color: 'var(--primary)' }, - { value: '40+', label: 'Projects shipped', color: 'var(--info)' }, - { value: '12', label: 'Satisfied patrons', color: 'var(--success)' }, + { value: "10+", label: "Years of craft", color: "var(--primary)" }, + { value: "40+", label: "Projects shipped", color: "var(--info)" }, + { value: "12", label: "Satisfied patrons", color: "var(--success)" }, ], projects: [ { - id: 'p1', - title: 'Nychthemeron', - color: 'primary', + id: "p1", + title: "Nychthemeron", + color: "primary", description: - 'A dual-theme design system — Hades (dark) and Apollo (light) — for classical, lasting interfaces.', - tags: ['Design System', 'Vue', 'CSS'], - link: '#', + "A dual-theme design system — Hades (dark) and Apollo (light) — for classical, lasting interfaces.", + tags: ["Design System", "Vue", "CSS"], + link: "#", }, { - id: 'p2', - title: 'The Oracle', - color: 'info', - description: 'A mythology-themed admin interface with consultations, petitioners, and prophecy tracking.', - tags: ['UI Kit', 'Dashboard', 'TypeScript'], - link: '#', + id: "p2", + title: "The Oracle", + color: "info", + description: + "A mythology-themed admin interface with consultations, petitioners, and prophecy tracking.", + tags: ["UI Kit", "Dashboard", "TypeScript"], + link: "#", }, { - id: 'p3', - title: 'Agora', - color: 'success', - description: 'A real-time marketplace for artisans, connecting makers with patrons across the ancient world.', - tags: ['Platform', 'Fullstack', 'Postgres'], - link: '#', + id: "p3", + title: "Agora", + color: "success", + description: + "A real-time marketplace for artisans, connecting makers with patrons across the ancient world.", + tags: ["Platform", "Fullstack", "Postgres"], + link: "#", }, ], skills: { - Design: ['Figma', 'Design Systems', 'Typography', 'Motion'], - Frontend: ['Vue', 'CSS', 'TypeScript', 'Vite'], - Backend: ['Node.js', 'Postgres', 'REST', 'GraphQL'], - Tools: ['Git', 'Storybook', 'Figma', 'Linear'], + Design: ["Figma", "Design Systems", "Typography", "Motion"], + Frontend: ["Vue", "CSS", "TypeScript", "Vite"], + Backend: ["Node.js", "Postgres", "REST", "GraphQL"], + Tools: ["Git", "Storybook", "Figma", "Linear"], }, social: { - github: '#', - linkedin: '#', - email: 'hello@example.com', + github: "#", + linkedin: "#", + email: "hello@example.com", }, -} +};