Small expense management app with a Flask backend and a Vite + React frontend.
Tech stack
- Backend: Flask, Flask-CORS, Flask-SQLAlchemy, Gunicorn
- Database: SQLite (default), supports PostgreSQL / MySQL via
DATABASE_URL - Frontend: React, Vite, Tailwind CSS
- Containerization: Docker, Docker Compose
- CI/CD: GitHub Actions (builds and publishes images to GHCR)
Prerequisites:
- Docker & Docker Compose
- Node.js + npm (if running frontend locally without Docker)
Run with Docker Compose (recommended):
docker compose build
docker compose up- Backend API: http://localhost:5000
- Frontend: http://localhost:8000
To run backend locally without Docker:
python -m venv .venv
source .venv/Scripts/activate # Windows: .venv\Scripts\activate
pip install -r backend/requirements.txt
cd backend
python app.pyTo run frontend locally without Docker:
cd frontend
npm ci
npm run dev-
GitHub Actions workflow:
.github/workflows/docker-publish.ymlbuilds both images and pushes to GitHub Container Registry (GHCR) as:ghcr.io/<your-username>/expense-system-backend:latestghcr.io/<your-username>/expense-system-frontend:latest
-
No extra secrets are required to publish to GHCR from the same repository; the workflow uses
GITHUB_TOKENwithpackages: writepermission.
When you create and publish a GitHub Release (for example tag v1.0.0), the repository runs the release-publish.yml workflow which:
- Builds backend and frontend images using the release tag and also tags
latest. - Pushes images to GHCR at
ghcr.io/<your-org>/expense-system-backend:<tag>andghcr.io/<your-org>/expense-system-frontend:<tag>. - Optionally mirrors images to Docker Hub if
DOCKERHUB_USERNAMEandDOCKERHUB_TOKENrepository secrets are set.
This creates an atomic, tagged image for every release and keeps latest updated for convenience.
- The backend reads
DATABASE_URLenv var. Set it to a Postgres or MySQL DSN for production. - Replace
SECRET_KEYwith a strong secret in production and configure secure credentials.
backend/— Flask API and modelsfrontend/— React + Vite appdocker-compose.yml— local compose configuration
- Add automated tests and a test job in CI.
- Add environment-specific compose files for staging/production.
- Configure an image registry retention policy if using GHCR.
- GitHub Container Registry (recommended with current workflow)
-
The existing workflow
.github/workflows/docker-publish.ymlbuilds images and pushes to GHCR as:ghcr.io/<your-username>/expense-system-backend:latestghcr.io/<your-username>/expense-system-frontend:latest
-
No extra secrets required;
GITHUB_TOKENis used. To pull images from other repos or orgs, configure permissions or use a PAT.
- Docker Hub
- To push to Docker Hub, create a repository for both images and add
DOCKERHUB_USERNAMEandDOCKERHUB_TOKENsecrets to GitHub, then modify the workflow todocker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }}and pushdocker.io/<user>/image:tag.
- Heroku (container-based deploy)
- Heroku accepts container images. Build images and push to Heroku Container Registry or use the Heroku GitHub integration.
- You already have a
Procfileinbackend/Procfile(web: gunicorn app:app), so deploying the backend as a Heroku app via the Python buildpack is also possible (push to Heroku git remote and setDATABASE_URL).
- Google Cloud Run / AWS ECS
- Build and push images to your registry (GCR / ECR / GHCR), then deploy to Cloud Run or ECS. Ensure env vars (like
DATABASE_URL,SECRET_KEY) are set in service configuration.
.dockerignore— reduces Docker build context size.env.example— example environment variables.github/workflows/docker-publish.yml— CI workflow to build & publish images
See also: TECHSTACK.md for detailed tech choices and recommended versions.
- Replace
SECRET_KEYwith a secure value in production env. - Set
DATABASE_URLto a managed database (Postgres recommended). - Use HTTPS at the frontend; configure reverse proxy or CDN.
- Add monitoring, log aggregation, and backups for DB.
- Replace image placeholders in
k8s/*-deployment.yamlwith your image path (GHCR or Docker Hub). - Create a secret for
SECRET_KEY:
kubectl create secret generic expense-secrets --from-literal=SECRET_KEY='replace-with-secret'- Apply manifests:
kubectl apply -f k8s/- Get the frontend external IP (may take a minute):
kubectl get svc expense-frontendI added a GitHub Actions workflow .github/workflows/deploy-cloudrun.yml that will build and push container images to Google Container Registry (GCR) and deploy them to Cloud Run when you push to main.
Required repository secrets (set these in GitHub Settings -> Secrets):
GCP_SA_KEY: JSON service account key with rolesroles/run.admin,roles/storage.admin,roles/iam.serviceAccountUser(base64 or raw JSON)GCP_PROJECT: your GCP project idGCP_REGION: Cloud Run region (e.g.us-central1)
After adding those secrets, pushing to main will automatically deploy both services to Cloud Run. Logs and status are available in the Actions tab.