📋 PERN Task Manager — Full CI/CD Pipeline
A full-stack PERN (PostgreSQL, Express, React, Node.js) task manager app deployed to AWS EC2 via a fully automated CI/CD pipeline using Jenkins, Docker, and Docker Hub.
Learning project — built to practice containerisation, CI/CD pipeline design, and production deployment on cloud infrastructure.
| Component | Technology |
|---|---|
| Source Control | GitLab |
| CI/CD Server | Jenkins on DigitalOcean Droplet |
| Image Registry | Docker Hub |
| Production Server | AWS EC2 (Amazon Linux 2023) |
| Database | PostgreSQL 16 (Docker) |
| Backend | Node.js + Express |
| Frontend | React + Nginx |
Checkout → Build Client → Build Server → Push Images → Deploy to EC2
- Checkout — Jenkins pulls latest code from GitLab
- Build Client — Builds React app Docker image, tags with build number and
:latest - Build Server — Builds Node.js API Docker image, tags with build number and
:latest - Push Images — Logs into Docker Hub, pushes both images
- Deploy to EC2 — SCPs
docker-compose.yamlto EC2, SSHs in and runsdocker compose pull && docker compose up -d
task-manager-cicd-pipeline/
│
├── client/ # React frontend
│ ├── public/
│ ├── src/
│ │ ├── components/
│ │ │ ├── InputTodo.js # Add todo form
│ │ │ ├── ListTodos.js # Todo list with delete
│ │ │ └── EditTodo.js # Edit modal
│ │ └── App.js
│ ├── nginx.conf # Nginx config — proxies /api to backend
│ ├── Dockerfile # Multi-stage: node build → nginx serve
│ └── package.json
│
├── server/ # Express backend
│ ├── index.js # API routes
│ ├── db.js # PostgreSQL connection pool
│ ├── database.sql # Table initialisation script
│ ├── Dockerfile
│ └── package.json
│
├── docker-compose.yaml # Production compose — uses pre-built images
├── Jenkinsfile # Full pipeline definition
└── README.md
Nginx as reverse proxy in the frontend container Rather than hardcoding an API URL into the React build, the frontend nginx proxies all /api/* requests to the backend container. This means the same Docker image works in any environment with zero config changes. nginxlocation /api { proxy_pass http://todo-backend:5000; } Jenkins SCPs docker-compose.yaml on every deploy The compose file lives in the repo and gets pushed to EC2 as part of the pipeline. The server never drifts out of sync with the codebase. Build number tagging with pinned production tags Every image is tagged with both the Jenkins build number and :latest during the build. The production docker-compose.yaml is updated by the pipeline to reference the exact build number tag — never :latest — so deployments are deterministic and rollback is as simple as reverting the tag to a previous build number. Automatic database initialisation The postgres container mounts server/database.sql into /docker-entrypoint-initdb.d/ so the todo table is created automatically on first run. No manual steps required on a fresh deployment. yamldb: volumes: - db-data:/var/lib/postgresql/data - ./server/database.sql:/docker-entrypoint-initdb.d/init.sql Health checks on all services All three containers report their real status rather than just Up. The backend and frontend are checked via HTTP, the database via pg_isready. Dependent services wait for healthy status before starting.
location /api {
proxy_pass http://todo-backend:5000;
}Jenkins SCPs docker-compose.yaml on every deploy The compose file lives in the repo and gets pushed to EC2 as part of the pipeline. The server never drifts out of sync with the codebase.
Build number tagging
Every image is tagged with both the Jenkins build number and :latest, enabling instant rollback by referencing a previous build number.
- Docker & Docker Compose
- Node.js 20+
# Clone the repo
git clone https://github.com/yourusername/task-manager-cicd-pipeline.git
cd task-manager-cicd-pipeline
# Create a .env file
cp .env.example .env
# Start everything
docker compose up --buildApp will be available at http://localhost.
Create a .env file in the ./server directory:
DB_HOST=db
DB_USER=postgres
DB_PASSWORD=yourpassword
DB_NAME=todo_db| Method | Endpoint | Description |
|---|---|---|
GET |
/api/todos |
Get all todos |
GET |
/api/todos/:id |
Get a single todo |
POST |
/api/todo |
Create a new todo |
PUT |
/api/todos/:id |
Update a todo |
DELETE |
/api/todos/:id |
Delete a todo |
To replicate this pipeline you will need:
- Jenkins running in Docker with the Docker socket mounted:
docker run -d --name jenkins \ -p 8080:8080 -p 50000:50000 \ -v jenkins_home:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /usr/bin/docker:/usr/bin/docker \ --group-add $(stat -c '%g' /var/run/docker.sock) \ jenkins/jenkins:lts - A
dockerhub-credscredential configured in Jenkins (username + password) - The EC2 private key copied into
/var/jenkins_home/.ssh/id_rsa - EC2 added to Jenkins
known_hostsviassh-keyscan
This was my first end-to-end CI/CD deployment. A full breakdown of every bug encountered and how it was fixed is documented in LEARNING_JOURNAL.md.
Key takeaways:
- Always build Docker images locally before pushing to CI
- Never hardcode credentials — duplicate JS object keys silently override env vars
- React runs in the browser, not the server —
localhostin fetch calls breaks in production docker compose psshowingUpdoes not mean the app is working — add health checks- Jenkins
Start of Pipeline / End of Pipelinewith no stages = Jenkinsfile not being read
Completed
- Automate database table creation via docker-entrypoint-initdb.d/
- Add Docker health checks to all services
- Pin image tags in production compose instead of using :latest
- Migrate pipeline from Jenkins to GitHub Actions
MIT