Compare commits

..

2 Commits

Author SHA1 Message Date
c07ff5edd8 Create DEPLOYMENT.md 2026-03-23 10:41:25 +05:30
e7043014a6 fix docker 2026-03-23 10:39:25 +05:30
4 changed files with 256 additions and 16 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(docker compose:*)",
"Bash(npx tsc:*)",
"Bash(curl -s http://127.0.0.1:8000/api/users/by-email/jeet.debnath2004@gmail.com)",
"Bash(ipconfig getifaddr:*)"
]
}
}

219
docs/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,219 @@
# Deployment Guide for Grateful Journal
## Overview
This guide covers deploying the Grateful Journal Docker stack to a production server. The app requires HTTPS — the Web Crypto API used for end-to-end encryption is blocked by browsers on plain HTTP.
---
## Deployment Options
### Option 1: VPS (Recommended) — DigitalOcean, Hetzner, Linode, Vultr
Full control. Run Docker Compose directly on the server behind a reverse proxy.
**Minimum specs:** 1 vCPU, 1 GB RAM, 20 GB disk
**Steps:**
1. Provision a server running Ubuntu 22.04+
2. Install Docker and Docker Compose
3. Point your domain DNS A record to the server IP
4. Set up a reverse proxy with SSL (see Reverse Proxy section below)
5. Clone the repo and configure environment files
6. Run `docker compose up --build -d`
---
### Option 2: Railway / Render / Fly.io
Platform-as-a-service. Easier setup but less control. These platforms handle SSL automatically.
- **Railway** — supports Docker Compose directly, good free tier
- **Render** — supports Docker, free tier available but spins down on inactivity
- **Fly.io** — supports Docker, generous free tier, good global distribution
Note: MongoDB on these platforms should be replaced with MongoDB Atlas (managed) since persistent volumes can be unreliable on free tiers.
---
### Option 3: Cloud VM (AWS EC2, GCP Compute, Azure VM)
Same as VPS but on a major cloud provider. More expensive for small apps but useful if you're already in that ecosystem.
---
## Reverse Proxy Setup (Required for HTTPS)
The frontend container must not be exposed directly. A reverse proxy handles SSL termination and forwards traffic to the frontend container.
### Using Nginx + Certbot (Let's Encrypt)
Install on the host (not inside Docker):
```bash
sudo apt install nginx certbot python3-certbot-nginx
```
Change `docker-compose.yml` to bind frontend to localhost only:
```yaml
ports:
- "127.0.0.1:8000:80"
```
Create `/etc/nginx/sites-available/grateful-journal`:
```nginx
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Enable and get SSL certificate:
```bash
sudo ln -s /etc/nginx/sites-available/grateful-journal /etc/nginx/sites-enabled/
sudo certbot --nginx -d yourdomain.com
sudo systemctl reload nginx
```
Certbot auto-renews the certificate. Done — the app is now on HTTPS.
### Using Traefik (Docker-native alternative)
Traefik runs as a Docker container and handles SSL automatically via Let's Encrypt. Better if you want everything inside Docker. Requires adding a `traefik` service to `docker-compose.yml` with labels on the frontend service.
---
## Environment Changes for Production
### `backend/.env`
```env
MONGODB_URI=mongodb://mongo:27017
MONGODB_DB_NAME=grateful_journal
API_PORT=8001
ENVIRONMENT=production
FRONTEND_URL=https://yourdomain.com
```
- Change `FRONTEND_URL` to your actual domain with `https://`
- This is used for CORS — must match exactly what the browser sends as the Origin header
### Root `.env` (frontend build args)
```env
VITE_FIREBASE_API_KEY=...
VITE_FIREBASE_AUTH_DOMAIN=...
VITE_FIREBASE_PROJECT_ID=...
VITE_FIREBASE_STORAGE_BUCKET=...
VITE_FIREBASE_MESSAGING_SENDER_ID=...
VITE_FIREBASE_APP_ID=...
VITE_API_URL=/api
```
- `VITE_API_URL=/api` stays as-is — nginx proxy handles routing
- Firebase keys stay the same unless you create a separate Firebase project for production
---
## Firebase Configuration
Firebase requires your production domain to be added as an **authorized domain** for Google Sign-In.
1. Go to [Firebase Console](https://console.firebase.google.com)
2. Select your project → Authentication → Settings → Authorized domains
3. Add `yourdomain.com`
Without this, Google sign-in will fail on the production domain.
---
## MongoDB Security
The current setup has no MongoDB authentication — fine for local dev, not for production.
Add a MongoDB username and password:
### `docker-compose.yml` — add environment to mongo service:
```yaml
mongo:
image: mongo:6
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: your_strong_password
...
```
### `backend/.env` — update the connection string:
```env
MONGODB_URI=mongodb://admin:your_strong_password@mongo:27017
```
Use a strong random password. Store it securely (not in git).
---
## Keeping Secrets Out of Git
Never commit `.env` files with real credentials. Before deploying:
- Add `.env` and `backend/.env` to `.gitignore` (already done)
- On the server, create the `.env` files manually or via a secrets manager
- Use environment variables injected by the platform if using Railway/Render/Fly.io
---
## Data Backups
MongoDB data lives in the `mongo_data` Docker volume. Back it up regularly:
```bash
# Dump
docker exec grateful-journal-mongo-1 mongodump --out /data/backup
docker cp grateful-journal-mongo-1:/data/backup ./mongo-backup
# Restore
docker cp ./mongo-backup grateful-journal-mongo-1:/data/backup
docker exec grateful-journal-mongo-1 mongorestore /data/backup
```
For automated backups, set up a cron job or use MongoDB Atlas which has built-in backups.
---
## Deploying Updates
After pushing code changes to the server:
```bash
git pull
docker compose up --build -d
```
This rebuilds only changed images and replaces containers with zero manual steps.
---
## Pre-Deployment Checklist
- [ ] Domain DNS pointing to server IP
- [ ] HTTPS set up via reverse proxy
- [ ] `FRONTEND_URL` updated to production domain in `backend/.env`
- [ ] Production domain added to Firebase authorized domains
- [ ] MongoDB authentication enabled
- [ ] `.env` files not committed to git
- [ ] `docker-compose.yml` frontend port bound to `127.0.0.1:8000:80`
- [ ] MongoDB backup strategy in place

View File

@@ -210,16 +210,27 @@ export function useOnboardingTour() {
clearPendingTourStep() clearPendingTourStep()
driverObj.destroy() driverObj.destroy()
}, },
onDestroyed: () => { onNextClick: () => {
const activeIndex = driverObj.getActiveIndex()
const steps = driverObj.getConfig().steps || []
// Last settings step → navigate to /
if (activeIndex === steps.length - 1) {
markOnboardingDone() markOnboardingDone()
clearPendingTourStep() clearPendingTourStep()
driverObj.destroy()
navigate('/')
return
}
driverObj.moveNext()
}, },
steps: getSettingsSteps(isMobile), steps: getSettingsSteps(isMobile),
}) })
driverRef.current = driverObj driverRef.current = driverObj
setTimeout(() => driverObj.drive(), 300) setTimeout(() => driverObj.drive(), 300)
}, []) }, [navigate])
return { startTour, continueTourOnHistory, continueTourOnSettings } return { startTour, continueTourOnHistory, continueTourOnSettings }
} }

View File

@@ -3,7 +3,7 @@
* Handles all communication with the backend API * Handles all communication with the backend API
*/ */
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8001' const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8001/api'
type ApiOptions = { type ApiOptions = {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
@@ -57,7 +57,7 @@ export async function registerUser(
}, },
token: string token: string
) { ) {
return apiCall('/api/users/register', { return apiCall('/users/register', {
method: 'POST', method: 'POST',
body: userData, body: userData,
token, token,
@@ -65,7 +65,7 @@ export async function registerUser(
} }
export async function getUserByEmail(email: string, token: string) { export async function getUserByEmail(email: string, token: string) {
return apiCall(`/api/users/by-email/${email}`, { token }) return apiCall(`/users/by-email/${email}`, { token })
} }
export async function updateUserProfile( export async function updateUserProfile(
@@ -73,7 +73,7 @@ export async function updateUserProfile(
updates: { displayName?: string; photoURL?: string; theme?: string }, updates: { displayName?: string; photoURL?: string; theme?: string },
token: string token: string
) { ) {
return apiCall(`/api/users/${userId}`, { return apiCall(`/users/${userId}`, {
method: 'PUT', method: 'PUT',
body: updates, body: updates,
token, token,
@@ -82,7 +82,7 @@ export async function updateUserProfile(
export async function deleteUser(userId: string, token: string) { export async function deleteUser(userId: string, token: string) {
return apiCall<{ message: string; user_deleted: number; entries_deleted: number }>( return apiCall<{ message: string; user_deleted: number; entries_deleted: number }>(
`/api/users/${userId}`, `/users/${userId}`,
{ {
method: 'DELETE', method: 'DELETE',
token, token,
@@ -125,7 +125,7 @@ export async function createEntry(
token: string token: string
) { ) {
return apiCall<{ id: string; message: string }>( return apiCall<{ id: string; message: string }>(
`/api/entries/${userId}`, `/entries/${userId}`,
{ {
method: 'POST', method: 'POST',
body: entryData, body: entryData,
@@ -141,7 +141,7 @@ export async function getUserEntries(
skip = 0 skip = 0
) { ) {
return apiCall<{ entries: JournalEntry[]; total: number }>( return apiCall<{ entries: JournalEntry[]; total: number }>(
`/api/entries/${userId}?limit=${limit}&skip=${skip}`, `/entries/${userId}?limit=${limit}&skip=${skip}`,
{ token } { token }
) )
} }
@@ -151,7 +151,7 @@ export async function getEntry(
entryId: string, entryId: string,
token: string token: string
) { ) {
return apiCall<JournalEntry>(`/api/entries/${userId}/${entryId}`, { return apiCall<JournalEntry>(`/entries/${userId}/${entryId}`, {
token, token,
}) })
} }
@@ -162,7 +162,7 @@ export async function updateEntry(
updates: Partial<JournalEntryCreate>, updates: Partial<JournalEntryCreate>,
token: string token: string
) { ) {
return apiCall(`/api/entries/${userId}/${entryId}`, { return apiCall(`/entries/${userId}/${entryId}`, {
method: 'PUT', method: 'PUT',
body: updates, body: updates,
token, token,
@@ -174,7 +174,7 @@ export async function deleteEntry(
entryId: string, entryId: string,
token: string token: string
) { ) {
return apiCall(`/api/entries/${userId}/${entryId}`, { return apiCall(`/entries/${userId}/${entryId}`, {
method: 'DELETE', method: 'DELETE',
token, token,
}) })
@@ -187,7 +187,7 @@ export async function getEntriesByDate(
token: string token: string
) { ) {
return apiCall<JournalEntry[]>( return apiCall<JournalEntry[]>(
`/api/entries/${userId}/date-range?startDate=${startDate}&endDate=${endDate}`, `/entries/${userId}/date-range?startDate=${startDate}&endDate=${endDate}`,
{ token } { token }
) )
} }
@@ -197,7 +197,7 @@ export async function getEntriesByDate(
export async function convertUTCToIST(utcTimestamp: string) { export async function convertUTCToIST(utcTimestamp: string) {
return apiCall<{ utc: string; ist: string }>( return apiCall<{ utc: string; ist: string }>(
`/api/entries/convert-timestamp/utc-to-ist`, `/entries/convert-timestamp/utc-to-ist`,
{ {
method: 'POST', method: 'POST',
body: { timestamp: utcTimestamp }, body: { timestamp: utcTimestamp },