A self-hosted, full-stack portfolio website with a built-in CMS — built with FastAPI, SQLite, and Jinja2. Manage your projects through a secure admin panel, deploy with Docker, and serve via Nginx with HTTPS.
- Public site — Homepage, projects overview, project detail pages, contact page
- Admin CMS — Full CRUD for projects (create, edit, delete, reorder)
- Markdown support — Write project descriptions in Markdown, rendered to HTML
- Security — Session-based auth, CSRF protection, login rate limiting, timing-safe credential checks
- Hero image carousel — Upload images to
static/prefixedhero_car_*and they appear automatically - Docker + Nginx — Production-ready stack with HTTPS via Let's Encrypt
- Visitor stats —
tracker.pyparses Nginx access logs and outputsstats.txt
| Layer | Technology |
|---|---|
| Backend | Python 3.12, FastAPI, SQLAlchemy |
| Templates | Jinja2 |
| Database | SQLite (persisted via Docker volume) |
| Frontend | Tailwind CSS (CDN), vanilla JS |
| Server | Uvicorn + Nginx |
| Deployment | Docker Compose, Let's Encrypt (Certbot) |
git clone https://github.com/your-username/your-portfolio.git
cd your-portfoliocp .env.example .envOpen .env and fill in your values:
SECRET_KEY=your-generated-secret-key
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your-strong-passwordGenerate a secure SECRET_KEY:
python -c "import secrets; print(secrets.token_urlsafe(32))"python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reloadVisit http://localhost:8000 — the admin panel is at /admin/login.
docker compose up --buildThe app is then available on port 8000 (Nginx not included in local mode — see deployment).
portfolio/
├── main.py # FastAPI app, routes, middleware
├── auth.py # Login, session, credential checks
├── database.py # SQLAlchemy engine and session
├── models.py # Project model with validators
├── logger_config.py # Rotating file + console logging
├── tracker.py # Nginx log parser → stats.txt
├── seed.py # One-time example data seeder
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── nginx.conf # Nginx reverse proxy config (edit domain/IP)
├── chmod-static.sh # Fix static dir permissions on VPS
├── vps-deploy-letsencrypt.sh # VPS deploy + Certbot helper
├── .env.example # Template for environment variables
├── templates/
│ ├── base.html
│ ├── index.html
│ ├── projects.html
│ ├── project_detail.html
│ ├── contact.html
│ ├── 404.html
│ └── admin/
│ ├── login.html
│ ├── dashboard.html
│ ├── editor.html
│ └── hero.html
└── static/
├── favicon.svg
├── hero_car_*.jpg # Homepage carousel images
└── uploads/hero/ # Admin-uploaded hero images
- VPS with Docker and Docker Compose installed
- A domain pointing to your server's IP
- Port 80 and 443 open
1. Update nginx.conf — replace andrew.co.nl and YOUR_SERVER_IP with your actual domain and server IP.
2. Upload the project to your VPS:
scp -r . user@YOUR_SERVER_IP:~/portfolio3. Set up environment:
ssh user@YOUR_SERVER_IP
cd ~/portfolio
cp .env.example .env
nano .env # Fill in your real values4. Obtain SSL certificate (first time):
docker compose run --rm --entrypoint "" nginx \
certbot certonly --webroot -w /var/www/certbot \
-d andrew.co.nl -d www.andrew.co.nl \
--email you@example.com --agree-tos --no-eff-email5. Deploy:
chmod +x vps-deploy-letsencrypt.sh
./vps-deploy-letsencrypt.shAdd images named hero_car_1.jpg, hero_car_2.jpg, etc. to the static/ folder — they appear automatically in the homepage carousel. Prefer .webp for better performance (the app will use .webp over .jpg if both exist).
The admin panel is available at /admin/login.
- Login with your
ADMIN_USERNAME/ADMIN_PASSWORDfrom.env - Dashboard — lists all projects with edit/delete controls
- Editor — create and edit projects with Markdown descriptions, tech stack, links, and images
- Login attempts are rate-limited to 5 per 15 minutes per IP
| Variable | Required | Description |
|---|---|---|
SECRET_KEY |
✅ | Random key for session signing (min. 32 chars) |
ADMIN_USERNAME |
✅ | Username for the admin panel |
ADMIN_PASSWORD |
✅ | Password for the admin panel |
LETSENCRYPT_ROOT |
optional | Path to Let's Encrypt certs (set by deploy script) |
MIT — feel free to use, adapt, and deploy as your own portfolio.