<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yoisen</title>
    <description>The latest articles on DEV Community by Yoisen (@yoisen06).</description>
    <link>https://dev.to/yoisen06</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4235275%2F47d25d2a-e340-4123-9ea6-622167bb6809.jpg</url>
      <title>DEV Community: Yoisen</title>
      <link>https://dev.to/yoisen06</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yoisen06"/>
    <language>en</language>
    <item>
      <title>Building CoinPulse: A Containerized Crypto Dashboard with Flask, PostgreSQL and Docker Compose</title>
      <dc:creator>Yoisen</dc:creator>
      <pubDate>Mon, 09 Nov 2026 14:01:49 +0000</pubDate>
      <link>https://dev.to/yoisen06/building-coinpulse-a-containerized-crypto-dashboard-with-flask-postgresql-and-docker-compose-jcg</link>
      <guid>https://dev.to/yoisen06/building-coinpulse-a-containerized-crypto-dashboard-with-flask-postgresql-and-docker-compose-jcg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6o3lpsv0gq5mxyx6aj9v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6o3lpsv0gq5mxyx6aj9v.png" alt=" " width="800" height="639"&gt;&lt;/a&gt;&lt;br&gt;
Building CoinPulse: A Containerized Crypto Dashboard with Flask, PostgreSQL and Docker Compose&lt;br&gt;
One command, three containers, and data that survives restarts.&lt;br&gt;
Why I built it&lt;br&gt;
For my Cloud Services course I wanted a project that needed more than one container and talked to something outside my own code. A crypto dashboard fit well: it needs live data from an external API, a database to keep user data, and a web interface. CoinPulse shows the top five cryptocurrencies by market cap in Indonesian Rupiah, with a personal watchlist saved in PostgreSQL. It is near-real-time (refreshed about every 60 seconds), not trading data. It also points in the direction I want for my career: cloud and DevOps engineering.&lt;/p&gt;

&lt;p&gt;Architecture&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fofnx4yjnmetyiw1dudgp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fofnx4yjnmetyiw1dudgp.png" alt=" " width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser only talks to Flask on port 5000. Flask calls the CoinGecko API and talks to PostgreSQL over a user-defined network, using the service name db. The API key stays on the server and never reaches the browser's JavaScript. PostgreSQL is not published to my machine, and its data lives in a named volume. Adminer, an optional database UI, sits in a Compose profile.&lt;br&gt;
The Dockerfile&lt;br&gt;
FROM python:3.12-slim-bookworm AS builder&lt;br&gt;
WORKDIR /build&lt;br&gt;
RUN python -m venv /opt/venv&lt;br&gt;
ENV PATH="/opt/venv/bin:$PATH"&lt;br&gt;
COPY requirements.txt .&lt;br&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;/p&gt;

&lt;p&gt;FROM python:3.12-slim-bookworm AS runtime&lt;br&gt;
ENV PATH="/opt/venv/bin:$PATH"&lt;br&gt;
RUN useradd --create-home --uid 1000 appuser&lt;br&gt;
WORKDIR /app&lt;br&gt;
COPY --from=builder /opt/venv /opt/venv&lt;br&gt;
COPY app/ .&lt;br&gt;
USER appuser&lt;br&gt;
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]&lt;/p&gt;

&lt;p&gt;(shortened; full file in the repo)&lt;br&gt;
Three decisions matter here. First, a multi-stage build: dependencies are installed in builder, and runtime copies only the virtual environment and the code. Second, a pinned base tag instead of latest. Third, requirements.txt is copied before the source, so Docker reuses the cached dependency layer when only code changes. I also used COPY instead of ADD (which can download URLs and extract archives, neither of which I needed), ran as a non-root user, and used the exec form of CMD. The final image is 228 MB.&lt;br&gt;
docker-compose.yml&lt;br&gt;
services:&lt;br&gt;
  app:&lt;br&gt;
    build: .&lt;br&gt;
    ports: ["5000:5000"]&lt;br&gt;
    environment:&lt;br&gt;
      DB_HOST: db              # service name, not an IP&lt;br&gt;
    depends_on:&lt;br&gt;
      db:&lt;br&gt;
        condition: service_healthy&lt;br&gt;
  db:&lt;br&gt;
    image: postgres:16.4-alpine&lt;br&gt;
    volumes:&lt;br&gt;
      - pgdata:/var/lib/postgresql/data&lt;br&gt;
    healthcheck:&lt;br&gt;
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]&lt;br&gt;
  adminer:&lt;br&gt;
    image: adminer:4.8.1&lt;br&gt;
    profiles: ["tools"]&lt;br&gt;
volumes:&lt;br&gt;
  pgdata:&lt;br&gt;
    name: coinpulse_pgdata&lt;/p&gt;

&lt;p&gt;(excerpt; the real file also sets the network, environment and restart policy)&lt;br&gt;
• Service-name DNS: on a custom network the app reaches the database as db. Container IPs change, and localhost inside the app container points to the app itself.&lt;br&gt;
• service_healthy: plain depends_on only waits for the container to start. The healthcheck waits until PostgreSQL accepts connections.&lt;br&gt;
• Named volume: the data stays when containers are removed.&lt;br&gt;
• Profile: Adminer only starts with --profile tools.&lt;br&gt;
• Secrets: values come from .env, which is git-ignored. Only .env.example is committed.&lt;br&gt;
Run it locally&lt;br&gt;
git clone &lt;a href="https://github.com/yoisen06/coinpulse.git" rel="noopener noreferrer"&gt;https://github.com/yoisen06/coinpulse.git&lt;/a&gt;&lt;br&gt;
cd coinpulse&lt;br&gt;
cp .env.example .env&lt;br&gt;
docker compose up -d --build&lt;/p&gt;

&lt;p&gt;Open &lt;a href="http://localhost:5000" rel="noopener noreferrer"&gt;http://localhost:5000&lt;/a&gt;. The database becomes healthy first, then the app starts.&lt;br&gt;
Does the data persist?&lt;br&gt;
I starred two coins, ran docker compose down, then docker compose up -d. The watchlist was still there, because it lives in the volume, not in the container. docker compose down -v would delete the volume and the data with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fos0i7kfnr9v342y4d2o6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fos0i7kfnr9v342y4d2o6.png" alt=" " width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I learned&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Test from a fresh clone. It worked on my machine, but my first clean clone returned 503: CoinGecko rejected the placeholder API key from .env.example. Now the app ignores empty or placeholder keys.&lt;/li&gt;
&lt;li&gt; Multi-stage is not automatically smaller. For this app, a single-stage image was 211 MB and multi-stage 228 MB, because every dependency is a prebuilt wheel. When I compiled psycopg2 from source it was 633 MB versus 222 MB. The pattern pays off when a compiler is needed.&lt;/li&gt;
&lt;li&gt; Healthy is not started. Waiting for a healthcheck removed the startup race between the app and the database.&lt;/li&gt;
&lt;li&gt; POSTGRES_PASSWORD is only read when the volume is first created. Changing it later needs docker compose down -v.
Links
• GitHub: &lt;a href="https://github.com/yoisen06/coinpulse" rel="noopener noreferrer"&gt;https://github.com/yoisen06/coinpulse&lt;/a&gt;
• Video demo: &lt;a href="https://youtu.be/9Mcwv53zMGY" rel="noopener noreferrer"&gt;https://youtu.be/9Mcwv53zMGY&lt;/a&gt;
I built this with help from an AI assistant (Claude); the README has the details.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>docker</category>
      <category>python</category>
    </item>
  </channel>
</rss>
