A Next.js 15 portfolio website for showcasing dog photos with AI-powered validation. Built for self-hosting on a Raspberry Pi using Docker, PostgreSQL, and Cloudflare Tunnel.
- Photo Gallery: Responsive grid with infinite scroll pagination
- Lightbox Viewing: Full-screen photo viewing with navigation
- AI Image Validation: NSFW detection + dog verification via FastAPI service
- Authentication: NextAuth with GitHub and Google OAuth
- User Profiles: View any user's profile and their uploaded photos
- Profile Editing: Users can customize their display name and profile picture
- Admin System: Configurable admin users who can delete any photo
- Photo Upload: Upload dog photos with AI validation and celebratory confetti
- Stats Dashboard: Live system monitoring with 3D Raspberry Pi model
- About Page: MDX-powered with lightbox galleries and interactive components
- Analytics: Umami self-hosted analytics integration
- PostgreSQL Database: Drizzle ORM with migration support
- Self-Hosted: Runs on Raspberry Pi with Docker + Nginx
- A Linux server (Raspberry Pi, Ubuntu, etc.)
- Domain name with Cloudflare DNS (for tunnel)
- GitHub and/or Google OAuth apps configured
- SSH access to your server
npm installcp .env.local.example .env.local
# Edit .env.local with your values# Start all services (Postgres, AI validator, Next.js) via Docker
npm run dev
# Or run separately:
docker compose up -d # Start services only
npm run db:push # Push database schema (first time)
npm run dev:next # Start Next.js dev serverVisit http://localhost:3000
npm run dev:stop-
Create your production env file locally:
cp .env.example .env.prod # Edit .env.prod with production values -
Sync env file to your server:
rsync -avz .env.prod pi@your-server:~/pi-site/.env.prod -
SSH into your server and deploy:
ssh your-server git clone https://github.com/rioredwards/pi-site.git ~/pi-site cd ~/pi-site chmod +x deploy.sh ./deploy.sh
The deployment script will:
- Install Docker if needed
- Set up Nginx reverse proxy
- Build and start all containers
- Run database migrations automatically
- Clean up old Docker images
# On your server
cd ~/pi-site
./update.shOr simply re-run ./deploy.sh - it's safe to run repeatedly.
pi-site/
βββ app/ # Next.js App Router
β βββ db/ # Database schema, client, and migrations
β β βββ schema.ts # Drizzle schema (photos, users tables)
β β βββ drizzle.ts # Database client
β β βββ actions.ts # Server actions
β β βββ migrations/ # SQL migration files
β βββ lib/ # Utilities and types
β βββ api/ # API routes
β βββ profile/ # User profile pages
β β βββ [userId]/ # View any user's profile
β β βββ edit/ # Edit own profile
β βββ stats/ # System stats dashboard
β βββ about/ # About page (MDX)
β βββ auth.ts # NextAuth configuration
βββ components/ # React components
βββ scripts/ # Build and deployment scripts
β βββ run-migrations.js # Production migration runner
β βββ docker-entrypoint.sh # Container startup script
βββ ai-img-validator/ # FastAPI image validation service
βββ docker-compose.yml # Production services
βββ docker-compose.dev.yml # Development services
βββ docker-compose.staging.yml # Staging services
βββ Dockerfile # Next.js app container
βββ deploy.sh # Initial deployment script
βββ update.sh # Update script
# Database
POSTGRES_USER=myuser
POSTGRES_PASSWORD=your-password
POSTGRES_DB=mydatabase
DATABASE_URL=postgres://myuser:password@db:5432/mydatabase
# NextAuth
AUTH_SECRET=your-secret-here
NEXTAUTH_URL=https://your-domain.com
GITHUB_CLIENT_ID=your-github-id
GITHUB_CLIENT_SECRET=your-github-secret
GOOGLE_CLIENT_ID=your-google-id
GOOGLE_CLIENT_SECRET=your-google-secret
# Image handling
PUBLIC_IMG_VALIDATOR_BASE_URL=http://ai-img-validator:8000 # prod
IMG_UPLOAD_DIR=/data/uploads/images # prod
# App keys
SECRET_KEY=your-secret-key
NEXT_PUBLIC_SAFE_KEY=your-public-key
# System Profiler (for /stats page)
SYSTEM_PROFILER_BASE_URL=http://system-profiler:8787 # prod
SYSTEM_PROFILER_AUTH_TOKEN=your-auth-token| Variable | Development | Production |
|---|---|---|
DATABASE_URL |
...@localhost:5432/... |
...@db:5432/... |
PUBLIC_IMG_VALIDATOR_BASE_URL |
http://localhost:8000 |
http://ai-img-validator:8000 |
IMG_UPLOAD_DIR |
./.data/uploads/images |
/data/uploads/images |
# Admin users (comma-separated provider-accountId format)
ADMIN_USER_IDS=github-123456,google-789012
# Umami Analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-website-id
NEXT_PUBLIC_UMAMI_URL=https://your-umami-instance.com- Frontend: Next.js 15, React 19, Tailwind CSS v4, Three.js
- Content: MDX for rich content pages
- Backend: Next.js Server Actions, NextAuth v4
- Database: PostgreSQL 17 + Drizzle ORM
- AI Service: FastAPI (Python) with NSFW + dog detection
- Analytics: Umami (self-hosted)
- Deployment: Docker, Nginx, Cloudflare Tunnel
- Icons: Lucide React, Hugeicons
# Development (Docker-based)
npm run dev # Start all services (Postgres, AI validator, etc.) via Docker
npm run dev:next # Start Next.js dev server only (if services already running)
npm run dev:stop # Stop dev services
npm run dev:logs # View dev service logs
# Staging (full stack locally in Docker)
npm run staging # Start all services in Docker (staging config)
npm run staging:stop # Stop staging
npm run staging:logs # View staging logs
npm run staging:reset # Wipe staging (including volumes)
# Database
npm run db:generate # Generate migrations from schema changes
npm run db:migrate # Run pending migrations
npm run db:push # Push schema directly (dev only)
npm run db:studio # Open Drizzle Studio
# Build
npm run build # Build for production
npm run start # Start production server
npm run lint # Run ESLint
npm run typecheck # Run TypeScript check- Make changes to
app/db/schema.ts - Generate migration:
npm run db:generate - Commit the migration files in
app/db/migrations/ - Deploy - migrations run automatically at container startup
# Quick schema sync (no migrations)
npm run db:push
# View database in browser
npm run db:studio
# Direct database access
docker exec -it pi_site_dev-db-1 psql -U myuser -d mydatabase# SSH into server
ssh your-server
cd ~/pi-site
# Access database
docker compose exec db psql -U myuser -d mydatabase- Go to https://github.com/settings/developers
- Create a new OAuth App
- Set Authorization callback URL to:
https://your-domain.com/api/auth/callback/github - Add Client ID and Secret to
.env.prod
- Go to https://console.cloud.google.com/apis/credentials
- Create OAuth 2.0 Client ID
- Add to Authorized redirect URIs:
https://your-domain.com/api/auth/callback/google - Add Client ID and Secret to
.env.prod
To make a user an admin:
- Sign in to the app
- Check your user ID in the database:
SELECT DISTINCT user_id FROM photos;
- Add your user ID to
.env.prod:ADMIN_USER_IDS=github-123456,google-789012
- Restart the app
Admins can delete any photo; regular users can only delete their own.
- Ensure DATABASE_URL uses
dbas hostname in production (Docker network) - Use
localhostonly in development - Restart containers:
docker compose down && docker compose up -d
- Ensure
AUTH_SECRETis set - Ensure
NEXTAUTH_URLmatches your actual domain - Restart containers after changing env vars
- Check AI validator is running:
docker compose ps - View logs:
docker compose logs ai-img-validator - Ensure images are < 5MB and are JPEG/PNG/WebP format
If Docker can't pull images due to IPv6 issues:
# Disable IPv6 for Docker
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"ip6tables": false,
"ipv6": false
}
EOF
sudo systemctl restart dockerThe deploy script automatically prunes old images. For manual cleanup:
docker image prune -f # Remove unused images
docker system prune -af # Remove everything unused (careful!)MIT
Based on the Next.js Self-Hosting Example by Lee Robinson.