Deploying AI Apps (FastAPI, Docker, Traefik)
How this site is deployed: multi-stage Docker build, FastAPI serving the React app and API, Traefik with automatic TLS, and GitHub Actions.
What it is
Deploying an AI app is deploying a web app plus three extra concerns: a model that takes seconds to load, API keys that must never be committed, and calls to paid services that need timeouts and fallbacks. Everything else is ordinary backend engineering, and that is the point: the AI part is a small piece of a system that has to be reliable in the usual ways.
This site runs as one container on a shared VPS. A multi-stage Dockerfile builds the React frontend with Node, then copies the compiled files into a Python image where FastAPI serves them at the root and the API under /api. Traefik, already running on the server for other projects, routes the domain to the container and manages the TLS certificate. A GitHub Actions workflow SSHes in on every push and runs docker compose up with a rebuild.
The decision that mattered most was not adding infrastructure the server did not need. It already had Traefik owning ports 80 and 443 for another site, so a separate Nginx and certbot setup would have conflicted. Joining the existing Docker network with the right labels took one compose file.
Why it's needed
A model that only runs on your laptop is a notebook, not a product. Recruiters and clients click the link. Deployment is also where the cost and reliability questions become real: what happens when OpenAI times out, when two requests arrive during model load, when a key leaks into a log.
For a career in AI engineering, shipping is the proof. Knowing Docker, reverse proxies, TLS, environment secrets and CI/CD is what turns a prototype into something a business can depend on, and it is what most AI demos lack.
How it works
- Serve one process: FastAPI mounts the built frontend as static files and exposes the API, so there is one container, one port and no CORS in production.
- Multi-stage Dockerfile: a Node stage runs the frontend build, a slim Python stage installs requirements and copies only the compiled assets. The final image has no Node in it.
- Load the model at import time, once, and consider baking the model weights into the image so a fresh container does not download them on first start.
- Secrets through environment variables from a .env file on the server that is never committed. The compose file reads it; the code reads os.environ.
- Traefik labels on the service declare the host rule and the certificate resolver. Traefik issues and renews the Let's Encrypt certificate automatically.
- CI/CD: a GitHub Actions job with a dedicated deploy-only SSH key pulls the repo on the server and runs docker compose up -d --build. Rollback is checking out the previous commit and rerunning.
Code example
# Dockerfile (multi-stage)
FROM node:20-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
FROM python:3.11-slim AS backend
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py knowledge_base.txt ./
COPY learn-hub ./learn-hub
RUN python learn-hub/build.py # static site generated at build time
COPY --from=frontend-build /app/frontend/dist ./frontend/dist
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml (excerpt): join the existing Traefik network
# services:
# backend:
# build: .
# env_file: .env
# networks: [n8n_default]
# labels:
# - traefik.enable=true
# - traefik.http.routers.portfolio.rule=Host(`abdurrehmanafzal.cloud`)
# - traefik.http.routers.portfolio.tls.certresolver=mytlschallenge
# - traefik.http.services.portfolio.loadbalancer.server.port=8000
# networks:
# n8n_default:
# external: true
Where I used this
This site: a multi-stage Dockerfile builds the React app and the Learning Hub, FastAPI serves everything from one container, Traefik on a shared Hostinger VPS routes the domain and manages TLS, and a GitHub Actions workflow redeploys on push over SSH. The voice agent is planned for its own subdomain with the same pattern.
Use cases
- Portfolio and demo sites that must stay up for recruiters
- Internal AI tools on a single VPS alongside other services
- Client chatbots delivered as one container with a compose file
- Voice agents on their own subdomain with a separate container
- Any FastAPI service that wraps a model and a frontend
Common mistakes
- Committing the .env file or printing keys in logs. Use environment variables, add .env to .gitignore before the first commit, and rotate any key that was ever pushed.
- Downloading model weights on every container start. It adds a slow cold start and a dependency on a third-party host during deploy; bake them into the image or a volume.
- No timeout on LLM calls. A hung upstream call ties up a worker forever. Set client timeouts and return a fallback.
- Running a second reverse proxy on a server that already has one. Check what owns ports 80 and 443 before planning.
- A deploy pipeline that cannot roll back. Tag images or keep the previous commit one command away.