The Stack
The Pipeline (5 Stages)
Stage 1: Pull & Build
Stage 2: Save & Transfer
Stage 3: Health Check
Stage 4: Zero-Downtime Swap
Stage 5: Cleanup
The Webhook Magic
Lessons Learned
Result Most dev teams I've worked with spend 30-60 minutes on each deployment. SSH into the server, pull the code, rebuild, restart services, pray nothing breaks. We got it down to 2 minutes. Fully automated. Zero SSH. Jenkins pulls the latest code and builds a Docker image. No npm install on the server — everything is baked into the image. Why not use a registry? For small teams, direct transfer is simpler and faster. Before deploying, we verify infrastructure is alive: If any check fails, the pipeline stops immediately. No half-deployed states. Push to dev branch → Jenkins builds and deploys to dev server.
Push to uat branch → Jenkins builds and deploys to UAT server. No manual triggers. No Slack messages asking "can someone deploy?" Don't check for containers that don't exist — Our UAT uses RDS, not a local DB container. The pipeline kept failing because it checked for global-db. Remove checks that don't apply to the environment. SSL is not optional — Even for internal APIs. RDS requires SSL by default. Set DB_SSL=true and move on. Use variables, not hardcoded values — redis://${REDIS_CONTAINER}:6379 not redis://global-redis:6379. Your future self will thank you. The best deployment is the one nobody has to think about. We use this pipeline at HEY!BOSS to deploy 100+ websites and multiple backend services. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
$ -weight: 500;">git pull → -weight: 500;">docker build -t app:latest .
-weight: 500;">git pull → -weight: 500;">docker build -t app:latest .
-weight: 500;">git pull → -weight: 500;">docker build -t app:latest .
-weight: 500;">docker save app:latest | gzip > app.tar.gz
scp app.tar.gz deploy-server:/opt/services/
-weight: 500;">docker save app:latest | gzip > app.tar.gz
scp app.tar.gz deploy-server:/opt/services/
-weight: 500;">docker save app:latest | gzip > app.tar.gz
scp app.tar.gz deploy-server:/opt/services/
-weight: 500;">docker -weight: 500;">stop app-old
-weight: 500;">docker rm app-old
-weight: 500;">docker load < app.tar.gz
-weight: 500;">docker run -d --name app --network shared-net app:latest
-weight: 500;">docker -weight: 500;">stop app-old
-weight: 500;">docker rm app-old
-weight: 500;">docker load < app.tar.gz
-weight: 500;">docker run -d --name app --network shared-net app:latest
-weight: 500;">docker -weight: 500;">stop app-old
-weight: 500;">docker rm app-old
-weight: 500;">docker load < app.tar.gz
-weight: 500;">docker run -d --name app --network shared-net app:latest
-weight: 500;">docker image prune -f
rm app.tar.gz
-weight: 500;">docker image prune -f
rm app.tar.gz
-weight: 500;">docker image prune -f
rm app.tar.gz - Jenkins — CI/CD orchestrator
- Docker — Containerized everything
- GitLab — Webhook triggers on push
- Nginx Proxy Manager — SSL + reverse proxy - Database container running?
- Redis container running?
- Network exists? - Don't check for containers that don't exist — Our UAT uses RDS, not a local DB container. The pipeline kept failing because it checked for global-db. Remove checks that don't apply to the environment.
- SSL is not optional — Even for internal APIs. RDS requires SSL by default. Set DB_SSL=true and move on.
- Use variables, not hardcoded values — redis://${REDIS_CONTAINER}:6379 not redis://global-redis:6379. Your future self will thank you. - Dev pushes code → 2 minutes later it's live
- UAT deployment → Same pipeline, different branch
- Rollback → Re-run previous build