A full-stack PERN (PostgreSQL, Express, React, Node.js) task manager app deployed to AWS EC2 via a fully automated CI/CD pipeline using GitHub Actions and Docker.
v2 of this project β previously deployed with Jenkins. Migrated to GitHub Actions to remove the need for a separate CI server. See task-manager-cicd-pipeline for the Jenkins version.
Developer β GitHub Push β GitHub Actions β Docker Hub β AWS EC2
β
Build & push Docker images
Pin exact build number tag in compose
SCP docker-compose.yaml to EC2
SSH into EC2 β docker compose up
| Component | Technology |
|---|---|
| Source Control | GitHub |
| CI/CD | GitHub Actions |
| Image Registry | Docker Hub |
| Production Server | AWS EC2 (Amazon Linux 2023) |
| Database | PostgreSQL 16 (Docker) |
| Backend | Node.js + Express |
| Frontend | React + Nginx |
build-and-push βββββββββββββββββββββββββββΊ deploy
βββ Checkout code βββ Checkout code
βββ Log in to Docker Hub βββ Pin image tags in compose
βββ Build & push client image βββ SCP compose file to EC2
βββ Build & push server image βββ SSH β docker compose up -d
The deploy job has needs: build-and-push β it only runs if the build succeeds. Both jobs run on fresh GitHub-hosted Ubuntu VMs.
task-manager-github-actions/
β
βββ .github/
β βββ workflows/
β βββ deploy.yml # Full pipeline definition
β
βββ client/ # React frontend
β βββ src/
β β βββ components/
β β β βββ InputTodo.js # Add todo β uses relative /api/todo
β β β βββ ListTodos.js # List + delete β uses relative /api/todos
β β β βββ EditTodo.js # Edit modal β uses relative /api/todos/:id
β β βββ App.js
β βββ nginx.conf # Proxies /api/* to backend container
β βββ Dockerfile # Multi-stage: node build β nginx serve
β βββ package.json
β
βββ server/ # Express backend
β βββ index.js # REST API routes β all prefixed /api
β βββ db.js # PostgreSQL connection with retry logic
β βββ database.sql # Table init script
β βββ Dockerfile
β βββ package.json
β
βββ database.sql # Mounted into postgres on fresh deploy
βββ docker-compose.yaml # Production compose β image tags pinned by pipeline
βββ README.md
GitHub Actions over Jenkins Jenkins requires a dedicated server running 24/7. GitHub Actions runs on GitHub's infrastructure β no server to maintain, no Docker socket to mount, no SSH keys to manage inside a container. The pipeline logic is identical, the operational overhead is zero.
Nginx reverse proxy in the frontend container
React fetch calls use relative URLs (/api/todos) with no hardcoded host or port. Nginx forwards all /api/* traffic to the backend container internally. The same Docker image works in any environment with zero config changes.
location /api {
proxy_pass http://todo-backend:5000;
}Build number pinning
The pipeline uses sed to replace image tags in docker-compose.yaml with the exact GitHub run number before deploying. Every production deployment references a specific immutable image β never :latest. Rollback is changing one number.
- name: Pin image tags in compose file
run: |
sed -i "s|image: .../server:.*|image: .../server:${{ github.run_number }}|" docker-compose.yaml
sed -i "s|image: .../client:.*|image: .../client:${{ github.run_number }}|" docker-compose.yamlAutomatic database initialisation
database.sql is SCP'd to EC2 alongside docker-compose.yaml and mounted into the postgres container via docker-entrypoint-initdb.d/. On a completely fresh deployment the table is created automatically β no manual steps.
Health checks on all services
All three containers report real status. Backend and frontend are checked via HTTP, database via pg_isready. Dependent services wait for healthy status before starting β the backend won't attempt DB connections until postgres is confirmed ready.
| Jenkins Version | GitHub Actions Version | |
|---|---|---|
| CI Server | Jenkins on DigitalOcean droplet | GitHub hosted runners |
| Server cost | ~$6/month droplet | Free |
| Pipeline file | Jenkinsfile (Groovy) |
deploy.yml (YAML) |
| Credentials | Jenkins credential store | GitHub Secrets |
| Docker access | Socket mount required | Built into runner |
| SSH to EC2 | Manual key setup in container | appleboy/ssh-action |
| Image tagging | BUILD_NUMBER |
github.run_number |
| Trigger | GitLab webhook | GitHub push event |
- Docker & Docker Compose
- Node.js 20+
# Clone the repo
git clone https://github.com/yourusername/task-manager-github-actions.git
cd task-manager-github-actions
# Create a .env file
cp .env.example .env
# Fill in your values
# Start everything
docker compose up --buildApp available at http://localhost.
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 |
Go to Settings β Secrets and variables β Actions and add:
| Secret | Value |
|---|---|
DOCKERHUB_USERNAME |
Your Docker Hub username |
DOCKERHUB_TOKEN |
Docker Hub access token (not password) |
EC2_HOST |
EC2 public IP or Elastic IP |
EC2_USER |
ec2-user |
EC2_SSH_KEY |
Private key contents (PEM format) |
# Install Docker
sudo yum update -y && sudo yum install -y docker
sudo systemctl start docker && sudo systemctl enable docker
sudo usermod -aG docker ec2-user
# Install Docker Compose plugin
sudo mkdir -p /usr/local/lib/docker/cli-plugins
sudo curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 \
-o /usr/local/lib/docker/cli-plugins/docker-compose
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
# Create deploy directory
mkdir -p ~/task-manager| Port | Protocol | Source | Purpose |
|---|---|---|---|
| 22 | TCP | 0.0.0.0/0 | GitHub Actions SSH |
| 80 | TCP | 0.0.0.0/0 | App access |
From migrating Jenkins β GitHub Actions:
- Pipeline concepts are identical across tools β triggers, jobs, steps, secrets. Learning one makes the next trivial.
- GitHub Actions prebuilt actions (
docker/login-action,appleboy/ssh-action) replace shell scripting boilerplate. Less code, fewer bugs. - Not needing a CI server eliminates an entire category of infrastructure problems β no Docker socket mounts, no SSH key management inside containers, no server maintenance.
needs:in GitHub Actions is more explicit than Jenkins stage ordering β you declare job dependencies intentionally.- A running container might be from an old image β always verify the tag matches your latest build number before debugging.
Carried over from Jenkins version:
- Always use relative URLs in React β
localhostin fetch calls breaks in production docker compose psshowingUpis not the same as healthy β always add health checks- Env vars with duplicate keys in JS objects silently use the last value β never hardcode credentials
- Migrated from Jenkins to GitHub Actions
- Automated database table creation via
docker-entrypoint-initdb.d/ - Docker health checks on all services
- Pinned image tags β exact build number deployed, never
:latest
- Provision EC2 infrastructure with Terraform
- Add security scanning β Trivy, Snyk, Checkov
- Add Prometheus + Grafana monitoring
- Migrate to Kubernetes deployment
MIT