Skip to content

Commit 8a6fe54

Browse files
github-actionsnikolaysm
authored andcommitted
build(docker)!: add dev compose + watch; harden prod compose; Next.js standalone runtime
- Makefile - Added `compose-watch` using `docker-compose.dev.yml` with `--watch --build` - Added `compose-watch-down` to stop dev stack and remove orphans - `compose-down` now uses `--remove-orphans` (safer than `-v`) - `run-db` starts Postgres from `docker-compose.dev.yml` - Backend image - Set `PYTHONPATH=/app`; copy `pyproject.toml` to expose app version - Switch CMD to `.venv/bin/uvicorn` for hermetic runtime - Keep non-root user; tidy ownership and entrypoint usage - Alembic & settings - Default DSN in `alembic.ini` simplified - Drop manual `sys.path.append` (rely on `PYTHONPATH`) - Add `db_echo` to `Settings`; engine `echo` reads `DB_ECHO` - Entrypoint - Robust Alembic loop with retries; gate via `RUN_MIGRATIONS=1` - Compose - New `docker-compose.dev.yml` with mounts and hot-reload (backend & frontend) - Production `docker-compose.yml`: - DB network set `internal: true`; Postgres port no longer published - Backend uses fixed container port 5432 in DSN - Frontend builds from `runner` target - Frontend - Dockerfile: pnpm multi-stage (`deps` → `builder` → `runner`), non-root user - Next.js `output: 'standalone'`; remove console in prod; ESM export of config - Tailwind config path now `tailwind.config.ts` - `/api/version`: load from `~/package.json` - `FileUpload`: move `cursor-pointer` to the Button BREAKING CHANGE: Default `docker-compose.yml` is production-oriented. Postgres is no longer exposed to the host and the DB network is internal. Use `docker-compose.dev.yml` (e.g., `make compose-watch` or `make run-db`) for local development with hot reload. Update any host-based DB tooling to connect via the dev compose file or through the container network.
1 parent 8c5ff90 commit 8a6fe54

File tree

16 files changed

+218
-114
lines changed

16 files changed

+218
-114
lines changed

Makefile

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ help:
1919
@echo " alembic-up Apply migrations (upgrade head)"
2020
@echo " compose-up docker compose up --build"
2121
@echo " compose-down docker compose down -v"
22+
@echo " compose-watch docker compose up --watch --build (dev)"
23+
@echo " compose-down-watch docker compose down --remove-orphans (dev)"
24+
@echo " run-db Start Postgres database (docker compose up -d postgres
2225
@echo " frontend-install Install frontend deps with pnpm"
2326
@echo " frontend-dev Run Next.js dev server"
2427
@echo " frontend-build Build Next.js production bundle"
@@ -51,7 +54,6 @@ run-backend: uv-setup uv-update
5154
[ -f "$$HOME/.local/bin/env" ] && . "$$HOME/.local/bin/env" || true; export PATH="$$HOME/.local/bin:$$PATH"; \
5255
cd backend && $(UV) run uvicorn app.main:app --reload
5356

54-
5557
.PHONY: alembic-rev
5658
alembic-rev: uv-setup
5759
@if [ -z "$(AUTOGEN)" ]; then echo "Usage: make alembic-rev AUTOGEN=message"; exit 1; fi
@@ -70,14 +72,18 @@ compose-up:
7072

7173
.PHONY: compose-down
7274
compose-down:
73-
@echo "[docker] Stopping and removing services + volumes"
74-
docker compose down -v
75+
@echo "[docker] Stopping and removing services + orphans"
76+
docker compose down --remove-orphans
7577

76-
# Add docker watchers for frontend and backend
7778
.PHONY: compose-watch
7879
compose-watch:
7980
@echo "[docker] Building and starting services with watchers"
80-
docker compose up --watch --build
81+
docker compose -f docker-compose.dev.yml up --watch --build
82+
83+
.PHONY: compose-watch-down
84+
compose-watch-down:
85+
@echo "[docker] Stopping and removing services + orphans"
86+
docker compose -f docker-compose.dev.yml down --remove-orphans
8187

8288
# Frontend (pnpm) helpers
8389
.PHONY: frontend-install
@@ -108,4 +114,4 @@ frontend-typecheck: frontend-install
108114
.PHONY: run-db
109115
run-db:
110116
@echo "[docker] Starting Postgres database"
111-
docker compose up -d postgres
117+
docker compose -f docker-compose.dev.yml up -d postgres

backend/Dockerfile

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
# 1) Builder stage: install uv and resolve dependencies once
44
FROM python:3.13-slim AS builder
5+
56
ENV PYTHONDONTWRITEBYTECODE=1 \
67
PYTHONUNBUFFERED=1 \
78
UV_LINK_MODE=copy
9+
810
WORKDIR /app
911

1012
RUN apt-get update \
@@ -22,14 +24,17 @@ RUN --mount=type=cache,target=/root/.cache/uv uv sync --no-dev
2224

2325
# 2) Runtime stage: copy only what we need and run as a non-root user
2426
FROM python:3.13-slim AS runtime
27+
2528
ENV PYTHONDONTWRITEBYTECODE=1 \
2629
PYTHONUNBUFFERED=1
30+
2731
WORKDIR /app
2832

2933
# Copy the virtual environment from builder and ensure it's used by default
3034
COPY --from=builder /app/.venv /app/.venv
3135
ENV VIRTUAL_ENV=/app/.venv
3236
ENV PATH="/app/.venv/bin:${PATH}"
37+
ENV PYTHONPATH=/app
3338

3439
# Create an unprivileged user
3540
RUN useradd -u 10001 -r -m appuser
@@ -38,19 +43,21 @@ RUN useradd -u 10001 -r -m appuser
3843
COPY app /app/app
3944
COPY alembic.ini /app/alembic.ini
4045
COPY alembic /app/alembic
46+
# Required to get application version
47+
COPY pyproject.toml /app/pyproject.toml
4148

4249
# Adjust ownership so the non-root user can access files
4350
RUN chown -R appuser:appuser /app
44-
USER appuser
4551

4652
# Expose the backend port
4753
EXPOSE 8000
4854

4955
# Add and use an entrypoint to handle DB wait + migrations
50-
USER root
5156
COPY --chown=appuser:appuser entrypoint.sh /app/entrypoint.sh
5257
RUN chmod +x /app/entrypoint.sh
58+
59+
# Switch to the non-root user
5360
USER appuser
5461

5562
ENTRYPOINT ["/app/entrypoint.sh"]
56-
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
63+
CMD [".venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

backend/alembic.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
script_location = alembic
44

55
# This value is overridden by env.py using Settings.DATABASE_URL
6-
sqlalchemy.url = postgresql+psycopg://budget_user:budget_pass@localhost:5432/budget_db
6+
sqlalchemy.url = postgresql+psycopg://user:pass@localhost:5432/db
77

88
[loggers]
99
keys = root

backend/alembic/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from sqlalchemy import engine_from_config, pool
1818
from sqlmodel import SQLModel
1919

20-
sys.path.append(str(Path(__file__).resolve().parents[1] / "app")) # noqa: E402
20+
# sys.path.append(str(Path(__file__).resolve().parents[1] / "app")) # noqa: E402
2121

2222
from app.core.config import get_settings # noqa: E402
2323
import app.models.transaction # noqa: F401 # ensure Transaction model is loaded

backend/app/core/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Settings(BaseSettings):
2323
# Database URL in SQLAlchemy format
2424
# Example: postgresql+psycopg://user:password@localhost:5432/budget_db
2525
database_url: str = "sqlite:///./budget_wise.db"
26+
db_echo: bool = True
2627

2728

2829
@lru_cache()

backend/app/db/session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
# environment variables.
2020
engine = create_engine(
2121
settings.database_url,
22-
echo=True,
22+
echo=settings.db_echo,
2323
pool_pre_ping=True,
2424
)
2525

backend/entrypoint.sh

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,21 @@ set -eu
33

44
echo "[entrypoint] Starting backend container..."
55

6-
# Optional: wait for the database to be ready if DATABASE_URL is provided
7-
if [ -n "${DATABASE_URL:-}" ]; then
8-
echo "[entrypoint] Waiting for database to be ready..."
9-
python - <<'PY'
10-
import os, sys, time
11-
try:
12-
import psycopg
13-
except Exception as e:
14-
# psycopg not installed? skip wait
15-
print("[entrypoint] psycopg not available; skipping DB wait", flush=True)
16-
sys.exit(0)
6+
RUN_MIGRATIONS=${RUN_MIGRATIONS:-1}
177

18-
url = os.environ.get("DATABASE_URL")
19-
if not url:
20-
sys.exit(0)
21-
22-
max_attempts = 30
23-
for i in range(max_attempts):
24-
try:
25-
with psycopg.connect(url, connect_timeout=3) as conn:
26-
print("[entrypoint] Database is ready.")
27-
break
28-
except Exception as e:
29-
print(f"[entrypoint] DB not ready yet ({i+1}/{max_attempts}): {e}")
30-
time.sleep(1)
31-
else:
32-
print("[entrypoint] Database is still not ready after waiting; exiting.", file=sys.stderr)
33-
sys.exit(1)
34-
PY
8+
if [ "$RUN_MIGRATIONS" = "1" ]; then
9+
echo "[entrypoint] Running Alembic migrations..."
10+
i=0
11+
until alembic -c /app/alembic.ini upgrade head; do
12+
i=$((i+1))
13+
[ "$i" -ge 10 ] && { echo "[entrypoint] Migrations failed after retries. Exiting." >&2; exit 1; }
14+
echo "[entrypoint] Alembic failed, retrying in 3s..."
15+
sleep 3
16+
done
17+
echo "[entrypoint] Migrations applied."
18+
else
19+
echo "[entrypoint] Skipping migrations (RUN_MIGRATIONS=$RUN_MIGRATIONS)."
3520
fi
3621

37-
# Run migrations (if any)
38-
echo "[entrypoint] Applying migrations (if any)..."
39-
alembic upgrade head || echo "[entrypoint] No migrations to apply"
40-
4122
echo "[entrypoint] Launching application: $@"
4223
exec "$@"

backend/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "budget-wise-backend"
3-
version = "0.3.0"
3+
version = "0.3.1"
44
description = "FastAPI backend for BudgetWise"
55
readme = "README.md"
66
requires-python = ">=3.11"

docker-compose.dev.yml

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
services:
2+
postgres:
3+
image: postgres:16-alpine
4+
container_name: budgetwise-postgres
5+
environment:
6+
- POSTGRES_DB=${DB_NAME}
7+
- POSTGRES_USER=${DB_USER}
8+
- POSTGRES_PASSWORD=${DB_PASSWORD}
9+
healthcheck:
10+
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
11+
interval: 5s
12+
timeout: 5s
13+
retries: 10
14+
volumes:
15+
- postgres_data:/var/lib/postgresql/data
16+
ports:
17+
- "${DB_PORT:-5432}:5432"
18+
networks:
19+
- db-network
20+
21+
backend:
22+
build: ./backend
23+
container_name: budgetwise-backend
24+
environment:
25+
- DATABASE_URL=postgresql+psycopg://${DB_USER}:${DB_PASSWORD}@postgres:${DB_PORT:-5432}/${DB_NAME}
26+
- PYTHONUNBUFFERED=1
27+
volumes:
28+
- ./backend:/app
29+
- backend_venv:/app/.venv
30+
ports:
31+
- "8000:8000"
32+
depends_on:
33+
postgres:
34+
condition: service_healthy
35+
networks:
36+
- db-network
37+
- webapp
38+
develop:
39+
watch:
40+
- action: restart
41+
path: ./backend/app
42+
- action: restart
43+
path: ./backend/alembic
44+
- action: restart
45+
path: ./backend/alembic.ini
46+
- action: rebuild
47+
path: ./backend/pyproject.toml
48+
49+
# Dev: run Next.js with hot reload; mount the source
50+
frontend:
51+
build:
52+
context: ./frontend
53+
target: deps # deps stage has pnpm available
54+
container_name: budgetwise-frontend
55+
working_dir: /app
56+
environment:
57+
- API_BASE_URL=${API_BASE_URL}
58+
- NEXT_TELEMETRY_DISABLED=1
59+
command: sh -lc 'test -d node_modules ||
60+
pnpm install --frozen-lockfile --yes;
61+
pnpm dev'
62+
volumes:
63+
- ./frontend:/app
64+
- frontend_node_modules:/app/node_modules
65+
ports:
66+
- "3000:3000"
67+
depends_on:
68+
- backend
69+
networks:
70+
- webapp
71+
develop:
72+
watch:
73+
- action: restart
74+
path: ./frontend
75+
ignore:
76+
- node_modules/
77+
- .next/
78+
- .turbo/
79+
- .git/
80+
- action: rebuild
81+
path: ./frontend/pnpm-lock.yaml
82+
- action: rebuild
83+
path: ./frontend/package.json
84+
85+
volumes:
86+
postgres_data:
87+
backend_venv:
88+
frontend_node_modules:
89+
90+
networks:
91+
db-network:
92+
driver: bridge
93+
internal: false
94+
webapp:
95+
driver: bridge

docker-compose.yml

Lines changed: 11 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,16 @@ services:
99
image: postgres:16-alpine
1010
container_name: budgetwise-postgres
1111
environment:
12-
- POSTGRES_DB=${DB_NAME}
13-
- POSTGRES_USER=${DB_USER}
14-
- POSTGRES_PASSWORD=${DB_PASSWORD}
12+
POSTGRES_DB: ${DB_NAME}
13+
POSTGRES_USER: ${DB_USER}
14+
POSTGRES_PASSWORD: ${DB_PASSWORD}
1515
healthcheck:
1616
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
1717
interval: 5s
1818
timeout: 5s
1919
retries: 10
2020
volumes:
2121
- postgres_data:/var/lib/postgresql/data
22-
ports:
23-
- "${DB_PORT:-5432}:5432"
2422
networks:
2523
- db-network
2624

@@ -29,12 +27,9 @@ services:
2927
container_name: budgetwise-backend
3028
environment:
3129
# PostgreSQL connection string (SQLAlchemy/SQLModel format)
32-
- DATABASE_URL=postgresql+psycopg://${DB_USER}:${DB_PASSWORD}@postgres:${DB_PORT}/${DB_NAME}
33-
- PYTHONUNBUFFERED=1
34-
volumes:
35-
# Mount the backend code to allow hot reloading in development.
36-
# Comment out in production builds to improve performance.
37-
- ./backend:/app
30+
DB_ECHO: ${DB_ECHO:-false}
31+
DATABASE_URL: postgresql+psycopg://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
32+
PYTHONUNBUFFERED: 1
3833
ports:
3934
- "8000:8000"
4035
depends_on:
@@ -43,49 +38,21 @@ services:
4338
networks:
4439
- db-network
4540
- webapp
46-
develop:
47-
watch:
48-
# Restart backend on source changes (bind mount already syncs files)
49-
- action: restart
50-
path: ./backend/app
51-
- action: restart
52-
path: ./backend/alembic
53-
- action: restart
54-
path: ./backend/alembic.ini
55-
# Rebuild image when dependencies definition changes
56-
- action: rebuild
57-
path: ./backend/pyproject.toml
5841

5942
frontend:
60-
build: ./frontend
43+
build:
44+
context: ./frontend
45+
target: runner
6146
container_name: budgetwise-frontend
6247
environment:
6348
API_BASE_URL: ${API_BASE_URL}
64-
volumes:
65-
# Mount the frontend code to allow hot reloading in development.
66-
# Comment out in production builds to improve performance.
67-
- ./frontend:/app
49+
NEXT_TELEMETRY_DISABLED: 1
6850
ports:
6951
- "3000:3000"
7052
depends_on:
7153
- backend
7254
networks:
7355
- webapp
74-
develop:
75-
watch:
76-
# Restart frontend server on source/config updates
77-
- action: restart
78-
path: ./frontend
79-
ignore:
80-
- node_modules/
81-
- .next/
82-
- .turbo/
83-
- .git/
84-
# Rebuild image when lockfile or package manifest changes
85-
- action: rebuild
86-
path: ./frontend/pnpm-lock.yaml
87-
- action: rebuild
88-
path: ./frontend/package.json
8956

9057
volumes:
9158
postgres_data:
@@ -94,6 +61,6 @@ volumes:
9461
networks:
9562
db-network:
9663
driver: bridge
97-
internal: ${INTERNAL_DB_NETWORK:-false}
64+
internal: true
9865
webapp:
9966
driver: bridge

0 commit comments

Comments
 (0)