A personal portfolio built from scratch with HTML, CSS, and JavaScript, without frontend frameworks, and enhanced with real DevOps practices: containerization, automated CI/CD, and deployment-ready infrastructure.
Local Code β Docker β GitHub Actions β Docker Hub
This isn't just a static website. It's a fully containerized application with an automated pipeline that builds, tags, and publishes a production-ready Docker image on every push. The goal is to demonstrate how DevOps engineering applies even to the simplest projects, turning a personal portfolio into a deployable, reproducible system.
| Layer | Technology |
|---|---|
| Frontend | HTML Β· CSS Β· JavaScript |
| Web Server | Nginx (Alpine) |
| Container | Docker |
| CI/CD | GitHub Actions |
| Registry | Docker Hub |
Portfolio/
βββ index.html # Main page
βββ style.css # Core styles
βββ mediaqueries.css # Responsive breakpoints
βββ script.js # Interactions & animations
βββ i18n.js # EN/ES language support
βββ assets/ # Images, PDFs, favicon
βββ Dockerfile # Container definition (nginx:alpine)
βββ nginx.conf # Custom Nginx config
βββ .dockerignore # Files excluded from the image
βββ .github/
β βββ workflows/
β βββ deploy.yml # CI/CD pipeline
βββ scripts/
βββ deploy.sh # Automated deployment script
The portfolio runs inside a lightweight Nginx Alpine container. The custom nginx.conf enables gzip compression, static asset caching, and security headers, with no hardcoded domains.
# Build the image
docker build -t portfolio-devops .
# Run the container
docker run -d -p 8080:80 --name portfolio portfolio-devopsOpen http://localhost:8080 and you're done.
# Stop & clean up
docker stop portfolio && docker rm portfolioEvery push to main triggers a GitHub Actions workflow that:
- Checks out the repository
- Builds the Docker image using Buildx with layer caching
- Tags it with
latestand the short commit SHA - Pushes it to Docker Hub
| Secret | Description |
|---|---|
DOCKER_USERNAME |
Your Docker Hub username |
DOCKER_PASSWORD |
Docker Hub access token or password |
π‘ Use a Docker Hub Access Token instead of your password for better security.
The project includes a ready-to-use deployment script at scripts/deploy.sh. It automates the full container lifecycle on any machine with Docker installed, by stopping the old container, pulling the latest image, and starting a new one.
#!/usr/bin/env bash
set -euo pipefail
IMAGE="${1:-portfolio-devops:latest}"
CONTAINER_NAME="portfolio"
HOST_PORT=80
echo "βΈ Stopping existing container (if any)..."
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm "$CONTAINER_NAME" 2>/dev/null || true
echo "βΈ Pulling latest image: $IMAGE"
docker pull "$IMAGE" || echo " (pull skipped β using local image)"
echo "βΈ Starting container on port $HOST_PORT..."
docker run -d \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-p "$HOST_PORT":80 \
"$IMAGE"
echo "β Portfolio is live at http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo 'localhost'):$HOST_PORT"chmod +x scripts/deploy.sh
./scripts/deploy.sh your-dockerhub-user/portfolio-devops:latestThe script works on any server with Docker (cloud VPS, local machine, or CI runner). Cloud deployment (AWS, etc.) is entirely optional.
The Nginx configuration uses server_name _ so it accepts requests on any domain out of the box. To connect a custom domain, just point a DNS A record to your server's public IP, with no server-side changes needed.
This project is more than a portfolio; it's a demonstration of how containerization, automation, and infrastructure thinking can be applied to any project, no matter how small. Every push is built, tagged, and published automatically. Every deployment is one command away.
Built with π οΈ by David Gamboa