A production-grade REST API built with Go, deployed on AWS EC2 with full CI/CD automation.
| Layer | Technology |
|---|---|
| Language | Go 1.26 |
| Router | Chi |
| Database | PostgreSQL (NeonDB — serverless managed) |
| ORM / Query | SQLC (type-safe generated queries) |
| Migrations | Goose |
| Container | Docker (multi-stage build) |
| Reverse Proxy | Nginx |
| Cloud | AWS EC2 (t3.micro, eu-west-1) |
| DNS / CDN / DDoS | Cloudflare |
| SSL | Cloudflare Origin Certificate (Full Strict) |
| CI/CD | GitHub Actions |
Client (Browser / Mobile)
│
▼
Cloudflare Edge
(DDoS protection, CDN, SSL termination)
│
▼ HTTPS (Cloudflare Origin Cert)
AWS EC2 — t3.micro (eu-west-1)
┌─────────────────────────────┐
│ Nginx (reverse proxy) │
│ Port 80 → redirect HTTPS │
│ Port 443 → proxy to :8080 │
│ │ │
│ ▼ │
│ Docker Container │
│ Go binary → port 8080 │
└─────────────────────────────┘
│
▼
NeonDB (Serverless PostgreSQL)
(managed, external, connection via DATABASE_URL)
Every push to main triggers an automated deployment via GitHub Actions.
Developer pushes to main
│
▼
GitHub Actions Runner (ubuntu-latest)
│
├── Checkout code
│
└── SSH into EC2
│
├── git fetch + reset --hard (sync with GitHub)
├── docker build (multi-stage, compiles Go binary)
├── docker stop + rm (remove old container)
├── docker run (start new container)
├── curl /health (verify deployment succeeded)
└── docker image prune (clean up old images)
Workflow file: .github/workflows/deploy.yml
The Dockerfile uses a two-stage build to keep the final image small (~15MB vs ~400MB):
Stage 1 (builder) Stage 2 (runtime)
───────────────── ─────────────────
golang:1.26-alpine → alpine:3.21
Full Go toolchain Compiled binary only
Source code ca-certificates
go mod download migrations/
go build ~15MB total
~400MB
Benefits:
- Smaller attack surface in production
- Faster image pulls
- No source code or build tools in production image
| Resource | Details |
|---|---|
| Cloud Provider | AWS (eu-west-1 — Ireland) |
| Instance Type | t3.micro (1 vCPU, 1GB RAM) |
| OS | Ubuntu 24.04 LTS |
| Elastic IP | Static public IP (survives instance stop/start) |
| Storage | 20GB gp3 EBS |
| Security Group | Ports 22, 80, 443 open |
- Proxy: ON (orange cloud) — real server IP is hidden
- SSL Mode: Full Strict — HTTPS encrypted end to end
- Certificate: Cloudflare Origin Certificate (15 year validity)
- Bot Fight Mode: OFF (API endpoints — JS challenges break API clients)
# Clone the repo
git clone https://github.com/pixprocoder/book-review-backend.git
cd book-review-backend
# Copy environment variables
cp .env.example .env
# Fill in your DATABASE_URL and other vars
# Run in development mode (hot reload)
make dev
# Run tests
make test
# Run linter
make lint# Apply migrations
make migrate-up
# Rollback last migration
make migrate-down
# Create new migration
make migrate-createDeployments are fully automated via GitHub Actions on push to main.
For manual deployment or first-time server setup:
# SSH into server
ssh -i your-key.pem devops@<EC2_IP>
# Navigate to app
cd /opt/apps/book-review-backend
# Pull latest code
git fetch origin main && git reset --hard origin/main
# Build and restart container
docker build -t book-review-backend:latest .
docker stop book-review && docker rm book-review
docker run -d \
--name book-review \
--restart unless-stopped \
-p 8080:8080 \
--env-file /opt/apps/book-review-backend/.env \
book-review-backend:latestMigrations are NOT run automatically on deploy. Run manually when schema changes:
ssh -i your-key.pem devops@<EC2_IP>
cd /opt/apps/book-review-backend
make migrate-upBase URL: https://book-review.pixprocoder.com
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
| ... | /api/v1/... |
Application routes |
DATABASE_URL=postgres://... # NeonDB connection string
# add other required vars from .env.example| Secret | Value |
|---|---|
EC2_HOST |
EC2 Elastic IP |
EC2_USER |
devops |
EC2_SSH_KEY |
Contents of your .pem private key |