I spent months burning money on Vercel and AWS before realizing I could run my entire stack on a single Hetzner VPS for €5/month. After moving huiro.dev and three client projects to this setup, I'm saving over €200/month while gaining full control over my infrastructure.
The Problem
Vercel's pricing hits hard once you scale beyond hobby projects. My main SaaS was costing €80/month just for hosting, plus another €40 for the database. Every client project added another €20-30/month. The serverless convenience wasn't worth €160/month when a VPS could handle the same load for €5.
But moving from serverless to VPS means handling SSL certificates, reverse proxies, container orchestration, and deployment pipelines yourself. I needed a setup that was both cost-effective and maintainable.
The Architecture
The solution uses Traefik as a reverse proxy with automatic SSL certificate generation, Docker Compose for container orchestration, and GitHub Actions for automated deployments. Everything runs on a single Hetzner VPS.
Next.js Deployment Architecture
Traefik handles incoming requests, automatically provisions SSL certificates via Let's Encrypt, and routes traffic to the appropriate Docker containers. Each Next.js app runs in its own container, making it easy to deploy multiple projects on the same server.
Step by Step
Set up the Hetzner VPS
First, create a CX11 instance (1 vCPU, 4GB RAM, €4.15/month) on Hetzner Cloud. Choose Ubuntu 22.04 LTS and add your SSH key during setup.
# SSH into your server
ssh root@your-server-ip
# Update system packages
apt update && apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
# Install Docker Compose
apt install docker-compose-plugin -y
# Create project directories
mkdir -p /opt/apps/huiro-blog
cd /opt/apps/huiro-blogConfigure Traefik
Create the Traefik configuration that handles SSL certificates and routing:
# docker-compose.yml
version: '3.8'
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./acme.json:/acme.json
networks:
- web
huiro-blog:
image: ghcr.io/mauriciocifuentes/huiro-blog:latest
container_name: huiro-blog
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.huiro-blog.rule=Host(`huiro.dev`)"
- "traefik.http.routers.huiro-blog.tls.certresolver=letsencrypt"
- "traefik.http.services.huiro-blog.loadbalancer.server.port=3000"
networks:
- web
networks:
web:
external: trueCreate Traefik Configuration
The main Traefik config handles SSL certificate generation and Docker integration:
# traefik.yml
api:
dashboard: false
entryPoints:
web:
address: ":80"
http:
redirections:
entrypoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
exposedByDefault: false
certificatesResolvers:
letsencrypt:
acme:
email: your-email@domain.com
storage: acme.json
httpChallenge:
entryPoint: webPrepare SSL Certificate Storage
Traefik needs a writable file to store SSL certificates:
# Create the acme.json file with correct permissions
touch acme.json
chmod 600 acme.json
# Create the Docker network
docker network create webBuild the Next.js Docker Image
Here's the production Dockerfile I use for all Next.js apps:
# Dockerfile
FROM node:18-alpine AS base
# Install dependencies
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --only=production
# Build the app
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]Set up GitHub Actions Deployment
Automate deployments with this GitHub Actions workflow:
# .github/workflows/deploy.yml
name: Deploy to Hetzner
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker build -t ghcr.io/${{ github.repository }}:latest .
docker push ghcr.io/${{ github.repository }}:latest
- name: Deploy to server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.HOST }}
username: root
key: ${{ secrets.PRIVATE_KEY }}
script: |
cd /opt/apps/huiro-blog
docker compose pull
docker compose up -d
docker system prune -fLaunch the Stack
Start all services and verify everything works:
# Start Traefik and your app
docker compose up -d
# Check logs
docker compose logs -f
# Verify SSL certificate
curl -I https://yourdomain.comResults
The cost difference is dramatic. My previous setup cost €160/month across Vercel, PlanetScale, and various serverless functions. Now I run:
- Main blog (huiro.dev)
- Two client SaaS applications
- Development staging environments
- PostgreSQL database
- Redis cache
All for €5/month on a single Hetzner CX11 instance. The server handles 50k monthly visitors without breaking a sweat, with average response times under 200ms.
SSL certificates renew automatically, deployments take 2 minutes via GitHub Actions, and I have full control over the environment.
Lessons Learned
• Start small, scale horizontally — One VPS handles way more than you think. Add more servers when you actually need them, not preemptively • Traefik simplifies everything — Automatic SSL and service discovery eliminate the biggest VPS pain points. No more nginx config hell • Docker labels are powerful — Traefik's Docker provider lets you configure routing entirely through compose labels. Zero separate config files • GitHub Container Registry is free — No need for DockerHub paid plans. GHCR gives unlimited private images for any GitHub repo • Monitor disk space closely — Docker images accumulate fast. Set up automated cleanup or your 20GB disk will fill up quickly
Takeaways
• A €5/month Hetzner VPS can replace hundreds of euros in serverless costs for most applications • Traefik + Docker Compose provides serverless-like convenience with VPS control and pricing • GitHub Actions + Container Registry creates a complete CI/CD pipeline without external services • This setup scales to multiple applications and environments on the same infrastructure
What's your biggest concern about moving from serverless to VPS hosting?
