Why I built this
I buy new skincare products often, and I always forget when I opened them, which ones didn't suit my skin, and when they expire. My notes app was a mess, so I built GlowLog: a small web app to log skincare products, mark whether they work for me, and see which ones expire within 30 days.
This was my midterm project for a Cloud Services course. The goal was not a big app but a clean containerized multi-tier setup: a web frontend, a REST API, and a database, all running with Docker Compose. I'm aiming for a Cloud/DevOps Engineer role, so this was good practice in packaging, orchestrating, and configuring services.
Architecture
GlowLog has three containers on one user-defined network:
- app: FastAPI serving both the web frontend (HTML, CSS, JavaScript) and the REST API on port 8000.
- db: PostgreSQL 16.4, reachable only inside the network, with a named volume for its data.
- adminer (optional): a database viewer that only starts under the
toolsprofile.
The browser loads the page from app, JavaScript calls the API with fetch, and app talks to the database using the service name db, not an IP address.
The Dockerfile
I used a multi-stage build:
FROM python:3.12.7-slim AS builder WORKDIR /build COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt FROM python:3.12.7-slim AS runtime WORKDIR /app COPY --from=builder /install /usr/local COPY . . USER appuser CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] This is simplified. The full file also sets ENV, EXPOSE, and creates appuser.
A few choices worth explaining:
- Pinned tag (
3.12.7-slim, notlatest) so builds are repeatable. - Dependencies are copied before the source code, so editing
main.pyreuses the cached install layer instead of downloading libraries again. - Multi-stage: the runtime stage starts clean and only takes the installed packages, so build leftovers never reach the final image.
-
COPYinstead ofADD, because I don't need URL downloads or automatic tar extraction. - Exec-form
CMDmakes uvicorn the main process, so it receives stop signals correctly.--host 0.0.0.0lets the app be reached from outside the container. - The app runs as a non-root user.
The final image is about 219MB on disk (53.6MB compressed). Honestly, for a pure-pip Python app the multi-stage savings are modest. The real value is the clean pattern and the slim base.
Docker Compose
services: db: image: postgres:16.4 volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] interval: 5s retries: 10 app: build: ./app ports: - "${APP_PORT}:8000" depends_on: db: condition: service_healthy This is simplified. The full file also has the network, environment variables, and Adminer.
- The healthcheck plus
service_healthymeans the app starts only when PostgreSQL can actually accept connections, not just when its container is running. - The named volume keeps data alive when containers are removed.
- Configuration comes from
.env. Only.env.exampleis committed, so no secrets reach GitHub. - Adminer uses a profile, so it stays off unless I run
docker compose --profile tools up -d.
Run it locally
git clone https://github.com/keylaghassani1-boop/glowlog.git cd glowlog cp .env.example .env # then set your own DB_PASSWORD docker compose up -d --build Open http://localhost:8000.
Testing persistence
I added products, ran docker compose down, and checked that docker volume ls still showed glowlog_pgdata. Then I ran docker compose up -d and the data was still there. docker compose down -v would have deleted the volume and wiped the data.
What I learned
- Containers are temporary, volumes are not. Data has to live in the right place.
- A healthcheck is more reliable than "container started" for service ordering.
- Using service names instead of IPs keeps the setup portable.
- Keeping secrets out of Git is a habit worth starting early.
- Tooling problems are part of the job: before writing any code, I had to get Docker Desktop's engine running on my machine.
Links
- GitHub: https://github.com/keylaghassani1-boop/glowlog
- Demo video: PASTE_YOUTUBE_LINK_HERE
AI disclosure: I used Claude for brainstorming, draft code, and troubleshooting. I reviewed, tested, and ran everything myself.