Small production-like full-stack project that demonstrates how to:
- Build a React + Node.js + PostgreSQL app
- Containerize services with Docker & Docker Compose
- Push images to Docker Hub
- Run the stack on an AWS EC2 instance
- Wire simple CI with GitHub Actions
The app shows the current time from PostgreSQL and provides a minimal Tasks CRUD API + UI.
-
Backend (Node.js + Express + PostgreSQL)
/– health check text response/api– returns current time from PostgreSQL (SELECT NOW())/api/tasks:GET /api/tasks– list all tasksPOST /api/tasks– create a new task ({ "title": "Buy milk" })PATCH /api/tasks/:id/toggle– toggleis_doneDELETE /api/tasks/:id– delete a task
- Connection retry logic on startup (waits for DB before starting API)
-
Database (PostgreSQL)
-
Uses official
postgres:16image -
On first start executes
db/init.sql, which creates table:CREATE TABLE IF NOT EXISTS tasks ( id SERIAL PRIMARY KEY, title TEXT NOT NULL, is_done BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );
-
-
Frontend (React + Nginx)
- Built with CRA (
react-scripts) - Production build served by
nginx:alpine - UI shows:
- Time from
/api - Tasks list with:
- create new task
- toggle “done / not done”
- delete task
- Loading & simple error states
- Time from
- Built with CRA (
-
Docker & Docker Compose
- Separate
Dockerfilefor backend and frontend docker-compose.yml– local development stack (builds images from source)docker-compose.prod.yml– production stack (pulls images from Docker Hub)- PostgreSQL data stored in named volumes (
pgdata,pgdata_prod)
- Separate
-
CI / CD
- CI workflow:
.github/workflows/ci.yml- Runs on push to
main - Installs dependencies & builds backend + frontend
- Runs on push to
- Docker images workflow:
.github/workflows/docker-ci-cd.yml- Builds and pushes:
DOCKERHUB_USERNAME/fullstack-app-backend:latestDOCKERHUB_USERNAME/fullstack-app-frontend:latest
- Uses GitHub Actions secrets for Docker Hub credentials
- Builds and pushes:
- CI workflow:
-
Deployment on AWS EC2
- Clone repo on EC2
- Fill
.envfrom.env.example docker compose -f docker-compose.prod.yml --env-file .env up -d- App is available at
http://<EC2_PUBLIC_IP>:3000
- Frontend: React, CRA, Nginx
- Backend: Node.js 18, Express,
pg - Database: PostgreSQL 16
- Infrastructure: Docker, Docker Compose
- CI / CD: GitHub Actions, Docker Hub
- Cloud: AWS EC2 (Ubuntu)
.
├─ backend/
│ ├─ Dockerfile
│ ├─ index.js
│ ├─ package.json
│ └─ package-lock.json
├─ frontend/
│ ├─ Dockerfile
│ ├─ nginx.conf
│ ├─ package.json
│ ├─ package-lock.json
│ └─ src/
│ ├─ App.js
│ └─ App.css
├─ db/
│ └─ init.sql
├─ .github/
│ └─ workflows/
│ ├─ ci.yml
│ └─ docker-ci-cd.yml
├─ docker-compose.yml
├─ docker-compose.prod.yml
├─ .env.example
└─ README.mdAll required variables are documented in .env.example. Copy it to .env and adjust values:
cp .env.example .envKey variables:
-
POSTGRES_DB– database name (default:fullstack_db) -
POSTGRES_USER– DB user (fullstack_user) -
POSTGRES_PASSWORD– DB password (fullstack_pass) -
BACKEND_PORT– backend port inside container (3001) -
DATABASE_URL– used by backend to connect to PostgreSQL:DATABASE_URL=postgres://fullstack_user:fullstack_pass@db:5432/fullstack_db
-
DOCKERHUB_USERNAME– your Docker Hub username (used in prod Compose + CI/CD)
Requirements:
- Docker
- Docker Compose v2
-
Clone the repository:
git clone git@github.com:AndriiDorohov/fullstack-app-devops-andrii.git cd fullstack-app-devops-andrii -
Create
.envfrom template:cp .env.example .env
For local development you can leave defaults from
.env.example. -
Start the stack:
docker compose up --build
-
Open in browser:
- Frontend: http://localhost:3000
- Backend API:
-
Stop the stack:
docker compose down
This is the same setup that runs on AWS EC2.
You can do it manually or via GitHub Actions.
Manual (local) build & push:
# Backend
docker build -t <your_dockerhub_username>/fullstack-app-backend:latest ./backend
docker push <your_dockerhub_username>/fullstack-app-backend:latest
# Frontend
docker build -t <your_dockerhub_username>/fullstack-app-frontend:latest ./frontend
docker push <your_dockerhub_username>/fullstack-app-frontend:latestMake sure DOCKERHUB_USERNAME in .env matches <your_dockerhub_username>.
On the target host (e.g. AWS EC2 Ubuntu):
git clone https://github.com/AndriiDorohov/fullstack-app-devops-andrii.git
cd fullstack-app-devops-andrii
cp .env.example .env
# Edit .env → set DOCKERHUB_USERNAME and, if needed, DB credentials
docker compose -f docker-compose.prod.yml --env-file .env up -dServices:
- Frontend:
http://<HOST_IP>:3000 - Backend API:
http://<HOST_IP>:3001/api - Tasks API:
http://<HOST_IP>:3001/api/tasks
docker-compose.prod.yml includes:
restart: unless-stoppedfor all services- Healthchecks:
- Backend:
curl -f http://localhost:3001/api - Frontend:
curl -f http://localhost/
- Backend:
Docker will automatically mark containers unhealthy and restart them if checks fail.
Workflow: .github/workflows/ci.yml
- Triggers:
pushtomain - Jobs:
- Installs Node.js 18
npm installinbackend/npm install&npm run buildinfrontend/
This ensures the app builds successfully before merging / deploying.
Workflow: .github/workflows/docker-ci-cd.yml
- Triggers:
pushtomain/workflow_dispatch - Steps:
- Login to Docker Hub using secrets:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN(Docker access token / password)
- Build and push:
DOCKERHUB_USERNAME/fullstack-app-backend:latestDOCKERHUB_USERNAME/fullstack-app-frontend:latest
- Login to Docker Hub using secrets:
To enable this workflow:
- Go to GitHub repo → Settings → Secrets and variables → Actions → New repository secret
- Add:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN
- Run workflow from Actions tab or push to
main.
Base URL (backend):
- Local dev:
http://localhost:3001 - Production (EC2):
http://<EC2_PUBLIC_IP>:3001
Endpoints:
-
GET /
Health check text message. -
GET /api
Returns current time from PostgreSQL:{ "time": "2025-12-10T18:21:03.000Z" } -
GET /api/tasks
Returns array of tasks:[ { "id": 1, "title": "Learn Docker", "is_done": false, "created_at": "2025-12-10T18:00:00.000Z" } ] -
POST /api/tasksRequest body:
{ "title": "New task" }Response:
{ "id": 2, "title": "New task", "is_done": false, "created_at": "2025-12-10T18:05:00.000Z" } -
PATCH /api/tasks/:id/toggle
Togglesis_doneflag and returns updated task. -
DELETE /api/tasks/:id
Deletes a task. Returns204 No Contenton success.
- Add authentication (JWT) and per-user tasks
- Add tests (Jest / Supertest) and extend CI pipeline
- Add monitoring (Prometheus + Grafana, Loki, etc.)
- Add staging environment or multi-env Compose configs
- Add Terraform / Ansible for full infra-as-code
This project is created for learning and portfolio purposes. Feel free to use it as a reference for your own full-stack + DevOps setups.