This is a walkthrough of the deployment setup behind my Gumroad clone: an EC2 box, Docker Compose, Nginx as the front door, and a GitHub Actions pipeline that does the boring parts so I don't have to SSH in and pray every time I ship a change.
The shape of it
Browser
│ HTTPS :443
▼
Nginx (host)
├─► /api/ → gumroad-server container (:5000)
├─► /uploads/ → shared volume on disk
└─► / → gumroad-client container (:5173)
gumroad-server ─► Redis (cache/sessions)
gumroad-server ─► MongoDB Atlas (or local :27017 fallback)
Nginx runs on the host, not in a container — it terminates SSL and decides where a request goes before Docker ever sees it. The client and server each live in their own container, and Redis rides along as a third. Mongo isn't containerized in production at all; it's Atlas, with a local instance as a dev/fallback option.
The pipeline: build, ship, prove it's alive
The GitHub Actions workflow has three jobs that gate each other — no point building Docker images if lint fails, no point deploying if the build fails.
Build & test — install deps, run lint. Fast fail if the code's broken.
Docker build — build the server and client images with Buildx, using GitHub Actions cache so rebuilds aren't starting from zero every time. Registry push is stubbed out (push: false) until a registry's wired up — right now the images just prove they can build.
Deploy — SSH into the box and do the actual work:
script: |
cd /home/ubuntu/Gumroad-Clone
git pull origin main
docker compose build --no-cache server client
docker compose down server client
docker compose up -d server client redis
sleep 10
STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/health)
if [ "$STATUS_CODE" -ne 200 ]; then
echo "Error: Backend failed healthcheck with status $STATUS_CODE"
docker compose logs server
exit 1
fi
docker exec gumroad-server npm run seed:products
echo "Deployment completed successfully! 🎉"
What to check after every deploy
GET /health → 200 (confirms the DB connection is live, not just that the process started)
GET /api/v1/products/discover → returns the seeded products
If you're deploying something similar, the whole thing is genuinely just: one host, a reverse proxy, a couple of containers, and a pipeline that refuses to lie to you about whether the deploy worked.

Top comments (0)