diff --git a/.gitignore b/.gitignore
index a21a015..fe0d04a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,11 @@ nextjs/**/node_modules/
# Angular
angular/**/.angular
-# End of https://www.toptal.com/developers/gitignore/api/macos
\ No newline at end of file
+
+## ------- Python -------
+venv/
+__pycache__/
+*.py[cod]
+*$py.class
+
+# End of https://www.toptal.com/developers/gitignore/api/macos
diff --git a/README.md b/README.md
index d2571d1..8cb114d 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ Docker Implementation
* ExpressJS
* Django
* Flask
+* Fastapi
+ * [Fastapi](https://github.com/agung-learns/docker-implementation/tree/main/fastapi/fastapi-docker)
### Contributors
diff --git a/fastapi/fastapi-docker/README.md b/fastapi/fastapi-docker/README.md
new file mode 100644
index 0000000..7606ebb
--- /dev/null
+++ b/fastapi/fastapi-docker/README.md
@@ -0,0 +1,37 @@
+Fastapi with Docker
+================================
+
+
+## How to Run
+
+### Local
+```shell
+docker compose up \
+ -f docker-compose.local.yaml --build -d
+
+fastapi dev cmd/cmd.py
+```
+
+### Production
+
+#### Docker
+```shell
+docker compose up -d --build
+```
+
+#### Kubernetes
+```shell
+kubectl apply -f deploy/k8s
+```
+
+## Contributors
+
diff --git a/fastapi/fastapi-docker/alembic.ini b/fastapi/fastapi-docker/alembic.ini
new file mode 100644
index 0000000..18f8896
--- /dev/null
+++ b/fastapi/fastapi-docker/alembic.ini
@@ -0,0 +1,117 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+# Use forward slashes (/) also on windows to provide an os agnostic path
+script_location = migrations
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
+# for all available tokens
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python>=3.9 or backports.zoneinfo library.
+# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to migrations/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+# version_path_separator = newline
+version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
+
+# set to 'true' to search source files recursively
+# in each "version_locations" directory
+# new in Alembic version 1.10
+# recursive_version_locations = false
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+sqlalchemy.url = driver://user:pass@localhost/dbname
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
+# hooks = ruff
+# ruff.type = exec
+# ruff.executable = %(here)s/.venv/bin/ruff
+# ruff.options = --fix REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/fastapi/fastapi-docker/api/app.py b/fastapi/fastapi-docker/api/app.py
new file mode 100644
index 0000000..607ed0c
--- /dev/null
+++ b/fastapi/fastapi-docker/api/app.py
@@ -0,0 +1,13 @@
+from fastapi import FastAPI
+
+
+def create_app() -> FastAPI:
+ app = FastAPI()
+ register_routers(app)
+ return app
+
+
+def register_routers(app: FastAPI) -> None:
+ from api.apps import user_router
+
+ app.include_router(user_router)
diff --git a/fastapi/fastapi-docker/api/apps/__init__.py b/fastapi/fastapi-docker/api/apps/__init__.py
new file mode 100644
index 0000000..63f9960
--- /dev/null
+++ b/fastapi/fastapi-docker/api/apps/__init__.py
@@ -0,0 +1 @@
+from .user import user_router
diff --git a/fastapi/fastapi-docker/api/apps/user/__init__.py b/fastapi/fastapi-docker/api/apps/user/__init__.py
new file mode 100644
index 0000000..609ad22
--- /dev/null
+++ b/fastapi/fastapi-docker/api/apps/user/__init__.py
@@ -0,0 +1,5 @@
+from fastapi import APIRouter
+
+user_router = APIRouter(tags=["user"])
+
+from . import views, models # noqa
diff --git a/fastapi/fastapi-docker/api/apps/user/models.py b/fastapi/fastapi-docker/api/apps/user/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/api/apps/user/views.py b/fastapi/fastapi-docker/api/apps/user/views.py
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/api/config/config.py b/fastapi/fastapi-docker/api/config/config.py
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/api/internal/__init__.py b/fastapi/fastapi-docker/api/internal/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/api/internal/database/__init__.py b/fastapi/fastapi-docker/api/internal/database/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/api/internal/database/database.py b/fastapi/fastapi-docker/api/internal/database/database.py
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/cmd/__init__.py b/fastapi/fastapi-docker/cmd/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/cmd/cmd.py b/fastapi/fastapi-docker/cmd/cmd.py
new file mode 100644
index 0000000..f49b8bf
--- /dev/null
+++ b/fastapi/fastapi-docker/cmd/cmd.py
@@ -0,0 +1,41 @@
+import subprocess
+
+import click
+import uvicorn
+from api.app import create_app
+
+app = create_app()
+
+
+@click.group()
+def cli():
+ pass
+
+
+@cli.command()
+@click.option("--host", default="localhost", help="Host to bind the server to.")
+@click.option("--port", default=8000, help="Port to bind the server to.")
+@click.option("--prod", default=False, help="Run in production mode.")
+def runserver(host: str, port: int, prod: bool) -> None:
+ click.echo(f"Starting server on {host}:{port}")
+ uvicorn.run(app, host=host, port=port)
+
+
+@cli.group()
+def migrations():
+ pass
+
+
+@migrations.command()
+def generate():
+ subprocess.run(["alembic", "revision", "--autogenerate"])
+
+
+@migrations.command()
+def up():
+ subprocess.run(["alembic", "upgrade", "head"])
+
+
+@migrations.command()
+def down():
+ subprocess.run(["alembic", "downgrade", "-1"])
diff --git a/fastapi/fastapi-docker/deploy/docker/Dockerfile b/fastapi/fastapi-docker/deploy/docker/Dockerfile
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/deploy/docker/entrypoint.sh b/fastapi/fastapi-docker/deploy/docker/entrypoint.sh
new file mode 100644
index 0000000..321cd44
--- /dev/null
+++ b/fastapi/fastapi-docker/deploy/docker/entrypoint.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+
+set -o errexit
+set -o pipefail
+set -o nounset
+
+postgres_ready() {
+python << END
+import sys
+
+import psycopg
+import urllib.parse as urlparse
+import os
+
+url = urlparse.urlparse(os.environ['DATABASE_URL'])
+dbname = url.path[1:]
+user = url.username
+password = url.password
+host = url.hostname
+port = url.port
+
+try:
+ psycopg.connect(
+ dbname=dbname,
+ user=user,
+ password=password,
+ host=host,
+ port=port
+ )
+except psycopg.OperationalError:
+ sys.exit(-1)
+sys.exit(0)
+
+END
+}
+until postgres_ready; do
+ >&2 echo 'Waiting for PostgreSQL to become available...'
+ sleep 1
+done
+>&2 echo 'PostgreSQL is available'
+
+
+vector_ready() {
+python << END
+import sys
+
+import psycopg
+import urllib.parse as urlparse
+import os
+
+url = urlparse.urlparse(os.environ['VECTOR_URL'])
+dbname = url.path[1:]
+user = url.username
+password = url.password
+host = url.hostname
+port = url.port
+
+try:
+ psycopg.connect(
+ dbname=dbname,
+ user=user,
+ password=password,
+ host=host,
+ port=port
+ )
+except psycopg.OperationalError:
+ sys.exit(-1)
+sys.exit(0)
+
+END
+}
+until vector_ready; do
+ >&2 echo 'Waiting for Vector to become available...'
+ sleep 1
+done
+>&2 echo 'Vector is available'
+
+exec "$@"
\ No newline at end of file
diff --git a/fastapi/fastapi-docker/deploy/docker/nginx/Dockerfile b/fastapi/fastapi-docker/deploy/docker/nginx/Dockerfile
new file mode 100644
index 0000000..e332ca7
--- /dev/null
+++ b/fastapi/fastapi-docker/deploy/docker/nginx/Dockerfile
@@ -0,0 +1,4 @@
+FROM nginx:1.25.3-alpine
+
+RUN rm /etc/nginx/conf.d/default.conf
+COPY nginx.conf /etc/nginx/conf.d
diff --git a/fastapi/fastapi-docker/deploy/docker/nginx/nginx.conf b/fastapi/fastapi-docker/deploy/docker/nginx/nginx.conf
new file mode 100644
index 0000000..8052b1d
--- /dev/null
+++ b/fastapi/fastapi-docker/deploy/docker/nginx/nginx.conf
@@ -0,0 +1,16 @@
+upstream api-server {
+ server api:8000;
+}
+
+server {
+ listen 80;
+ location / {
+ proxy_pass http://api-server;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+ proxy_redirect off;
+ }
+ location /upload/ {
+ alias /app/upload/;
+ }
+}
diff --git a/fastapi/fastapi-docker/deploy/docker/start.sh b/fastapi/fastapi-docker/deploy/docker/start.sh
new file mode 100644
index 0000000..fbdb46d
--- /dev/null
+++ b/fastapi/fastapi-docker/deploy/docker/start.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+set -o errexit
+set -o pipefail
+set -o nounset
+
+alembic upgrade head
+uvicorn cmd.cmd:app --reload --reload-dir api --host 0.0.0.0 --port 8000
\ No newline at end of file
diff --git a/fastapi/fastapi-docker/deploy/k8s/api-deployment.yaml b/fastapi/fastapi-docker/deploy/k8s/api-deployment.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/deploy/k8s/api-service.yaml b/fastapi/fastapi-docker/deploy/k8s/api-service.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/deploy/k8s/beat-deployment.yaml b/fastapi/fastapi-docker/deploy/k8s/beat-deployment.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/deploy/k8s/db-deployment.yaml b/fastapi/fastapi-docker/deploy/k8s/db-deployment.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/deploy/k8s/db-service.yaml b/fastapi/fastapi-docker/deploy/k8s/db-service.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/deploy/k8s/db-volume.yaml b/fastapi/fastapi-docker/deploy/k8s/db-volume.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/deploy/k8s/k8s-env.example b/fastapi/fastapi-docker/deploy/k8s/k8s-env.example
new file mode 100644
index 0000000..622c03e
--- /dev/null
+++ b/fastapi/fastapi-docker/deploy/k8s/k8s-env.example
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: gap-ai-env
+ namespace: prod
+type: Opaque
+data:
+ DATABASE_URL:
diff --git a/fastapi/fastapi-docker/deploy/k8s/nginx-service.yaml b/fastapi/fastapi-docker/deploy/k8s/nginx-service.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/fastapi/fastapi-docker/docker-compose.local.yaml b/fastapi/fastapi-docker/docker-compose.local.yaml
new file mode 100644
index 0000000..844b580
--- /dev/null
+++ b/fastapi/fastapi-docker/docker-compose.local.yaml
@@ -0,0 +1,17 @@
+version: '3'
+
+services:
+ db:
+ container_name: "fastapi-local-db"
+ image: "postgres:16.2-alpine"
+ environment:
+ - POSTGRES_USER=gapai_user
+ - POSTGRES_PASSWORD=gapai_password
+ - POSTGRES_DB=gapai_db
+ volumes:
+ - api_local_db:/var/libs/postgresql/data/
+ ports:
+ - "5432:5432"
+
+volumes:
+ api_local_db: {}
diff --git a/fastapi/fastapi-docker/docker-compose.yaml b/fastapi/fastapi-docker/docker-compose.yaml
new file mode 100644
index 0000000..9dac1fb
--- /dev/null
+++ b/fastapi/fastapi-docker/docker-compose.yaml
@@ -0,0 +1,38 @@
+version: '3.8'
+
+services:
+ nginx:
+ container_name: "gap-ai-nginx"
+ build: deploy/docker/nginx
+ ports:
+ - "80:80"
+ depends_on:
+ - api
+
+ api:
+ container_name: "fastapi-api"
+ build:
+ context: .
+ dockerfile: deploy/docker/Dockerfile
+ command: /app/start.sh
+ volumes:
+ - gap_ai_upload_files:/app/upload
+ env_file:
+ - .env-prod
+ depends_on:
+ - redis
+ - db
+ - vector
+
+ db:
+ container_name: "fastapi-db"
+ image: postgres:16.2-alpine
+ environment:
+ - POSTGRES_USER=gapai_user
+ - POSTGRES_PASSWORD=gapai_password
+ - POSTGRES_DB=gapai_db
+ volumes:
+ - api_db:/var/lib/postgresql/data/
+
+volumes:
+ api_db: {}
diff --git a/fastapi/fastapi-docker/manage.py b/fastapi/fastapi-docker/manage.py
new file mode 100644
index 0000000..4924fb1
--- /dev/null
+++ b/fastapi/fastapi-docker/manage.py
@@ -0,0 +1,4 @@
+from cmd.cmd import cli
+
+if __name__ == "__main__":
+ cli()
diff --git a/fastapi/fastapi-docker/migrations/README b/fastapi/fastapi-docker/migrations/README
new file mode 100644
index 0000000..98e4f9c
--- /dev/null
+++ b/fastapi/fastapi-docker/migrations/README
@@ -0,0 +1 @@
+Generic single-database configuration.
\ No newline at end of file
diff --git a/fastapi/fastapi-docker/migrations/env.py b/fastapi/fastapi-docker/migrations/env.py
new file mode 100644
index 0000000..36112a3
--- /dev/null
+++ b/fastapi/fastapi-docker/migrations/env.py
@@ -0,0 +1,78 @@
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline() -> None:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online() -> None:
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ connectable = engine_from_config(
+ config.get_section(config.config_ini_section, {}),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection, target_metadata=target_metadata
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/fastapi/fastapi-docker/migrations/script.py.mako b/fastapi/fastapi-docker/migrations/script.py.mako
new file mode 100644
index 0000000..fbc4b07
--- /dev/null
+++ b/fastapi/fastapi-docker/migrations/script.py.mako
@@ -0,0 +1,26 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision: str = ${repr(up_revision)}
+down_revision: Union[str, None] = ${repr(down_revision)}
+branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
+depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/fastapi/fastapi-docker/requirements.txt b/fastapi/fastapi-docker/requirements.txt
new file mode 100644
index 0000000..b8c48b4
--- /dev/null
+++ b/fastapi/fastapi-docker/requirements.txt
@@ -0,0 +1,3 @@
+fastapi[standard]==0.115.0
+uvicorn[standard]==0.31.1
+alembic==1.13.3
\ No newline at end of file