Containerized Portfolio with CI/CD
This site, packaged as an nginx container image. GitHub Actions checks it on every push, smoke-tests the image and publishes versioned tags to GitHub Container Registry.
Problem
A static site is easy to deploy by hand and just as easy to deploy inconsistently: there is no versioned artifact, no record of which build is live, and no known-good version to return to.
Why I built it. I wanted a small project where I own the whole path from commit to a versioned container image, using the same pieces a larger service would use.
How it works
Push to main
A commit on main starts the workflow. It can also be started by hand with workflow_dispatch.
Approach
The site is built into plain static files, copied into an nginx:alpine image, and published to GHCR by GitHub Actions on every push to main.
docker run -p 8080:80 ghcr.io/garvg4278/garv-portfolio:latestTechnology and why
- Multi-stage Docker build
- Node and the build tooling stay in the first stage. The final image contains only nginx and the built files.
- nginx:alpine
- A small, standard static server. Caching, security headers and redirects live in one config file, nginx.conf.
- GitHub Actions
- The pipeline lives in the same repository as the code, so changes to either are reviewed together.
- GHCR
- The image sits next to the source and is addressable by tag, including the commit SHA.
What I implemented
- Wrote the original Dockerfile and workflow: a single-stage nginx:alpine image, a GHCR login using a personal access token stored as a repository secret, and a push of the latest and short-SHA tags.
- Rebuilt the site in Astro and Three.js in October 2026, with content in data files, shared components and a small logic layer with unit tests.
- Moved the Dockerfile to a multi-stage build and added an nginx configuration: long-lived caching for hashed assets, no-cache for HTML, gzip, security headers, a custom 404 page and redirects from the old URLs.
- Extended the workflow with lint, type-check, tests, a link check and an image smoke test before anything is pushed.
Decisions and trade-offs
- Kept nginx:alpine on port 80 so the docker run command already documented in the README still works.
- Kept the GHCR login on the existing personal access token (GHCR_PAT) rather than changing registry permissions during the rebuild.
Testing and verification
- CI runs ESLint, astro check, unit tests, a production build and a check of every internal link, asset and anchor in the generated pages.
- Each image carries the commit SHA as a tag, so a running container can be traced back to a commit.
Results
- Every push to main produces two image tags in GHCR: latest and the short commit SHA.
Limitations and next steps
- The workflow publishes the image. It does not roll the new image out to the host; that step happens outside this repository.
- There are no automated browser or accessibility tests in CI yet.
- The registry login uses a long-lived personal access token instead of the workflow token.