Backend API built with FastAPI, SQLAlchemy, Alembic, and PostgreSQL.
This README is written for the FastAPI Backend project setup:
- FastAPI app entrypoint:
app.main:app - ORM: SQLAlchemy
- Migrations: Alembic
- Database: PostgreSQL
- Local config file:
.env.local - Docker config file:
.env.docker - Docker Compose files:
docker-compose-dev.ymldocker-compose-wsl.yml
- Production hosting: Render
- Production database: Neon
- Optional direct Neon URL for migrations:
DIRECT_DATABASE_URL
This project is designed so that:
- local Python runs default to
.env.local - Docker containers receive environment variables from
.env.docker - Alembic can use
DIRECT_DATABASE_URLfor Neon migrations when provided - the API uses
uvicornin development andgunicornin production
This project uses a centralized Settings class in config.py to manage environment-based configuration.
The recommended pattern is:
- define all required application settings in one place
- load local development values from
.env.local - allow containerized environments to override values through injected environment variables
- support special cases such as tests or CI with an optional
ENV_FILEoverride - expose computed properties for derived values such as database URLs
With this setup:
- Local development defaults to
.env.local - Docker Compose can inject
.env.docker, which overrides local values inside the container - Tests and CI can set
ENV_FILEto point to a different environment file when needed
The configuration layer also supports:
- an optional direct database URL override for migrations or other non-standard environments
- automatic switching to a test database name when
testing=true - a separate resolved database URL for normal application use versus migrations
-
Run locally with the default local environment file:
uvicorn app.main:app --reload
-
Run in Docker with container-provided environment variables
-
Run tests or CI with a custom environment file:
ENV_FILE=.env.test pytest
This keeps configuration:
- consistent, by defining settings in one place
- flexible, by supporting local, Docker, test, and CI environments
- maintainable, by avoiding repeated connection-string logic across the codebase
The project is organized as follows:
.
├── .github/
│ └── workflows/
│ └── CI-CD.yml
├── alembic/
│ ├── versions/
│ ├── env.py
│ ├── README
│ └── script.py.mako
├── app/
│ ├── routers/
│ ├── __init__.py
│ ├── config.py
│ ├── database.py
│ ├── main.py
│ ├── models.py
│ ├── oauth2.py
│ ├── schemas.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_auth.py
│ ├── test_posts.py
│ ├── test_users.py
│ ├── test_utils.py
│ └── test_votes.py
├── .dockerignore
├── .env.docker
├── .env.local
├── .env.prod
├── .gitignore
├── alembic.ini
├── docker-compose-dev.yml
├── docker-compose-wsl.yml
├── Dockerfile
├── gunicorn.service
├── nginx.conf
└── requirements.txt
Do not commit real secrets to GitHub.
Keep .env.local, .env.docker, and any production secrets out of version control unless they contain only placeholders.
Use this for local Python runs.
DATABASE_HOSTNAME=localhost
DATABASE_PORT=5432
DATABASE_NAME=fastapi
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=yourpassword
SECRET_KEY=replace_with_a_real_secret
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30Use this for Docker containers.
DATABASE_HOSTNAME=postgres
DATABASE_PORT=5432
DATABASE_NAME=fastapi
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=yourpassword
SECRET_KEY=replace_with_a_real_secret
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
# Postgres container variables
POSTGRES_USER=postgres
POSTGRES_PASSWORD=yourpassword
POSTGRES_DB=fastapiSet these in the Render dashboard, not in a committed file:
DATABASE_HOSTNAME=...
DATABASE_PORT=5432
DATABASE_NAME=...
DATABASE_USERNAME=...
DATABASE_PASSWORD=...
DATABASE_SSLMODE=require
SECRET_KEY=replace_with_a_real_secret
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
DIRECT_DATABASE_URL=postgresql://.../neondb?sslmode=require&channel_binding=requirepython -m venv venv
venv\Scripts\activate
pip install -r requirements.txtpython3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtMake sure your .env.local exists before starting the app.
Use this path if PostgreSQL is installed directly on your machine and .env.local points to it.
uvicorn app.main:app --reloadalembic upgrade headalembic revision --autogenerate -m "describe your change"alembic downgrade -1http://127.0.0.1:8000/docs
Use this when both the API and PostgreSQL run in containers.
docker compose -f docker-compose-dev.yml up --builddocker compose -f docker-compose-dev.yml up --build -ddocker compose -f docker-compose-dev.yml downdocker compose -f docker-compose-dev.yml down -vdocker compose -f docker-compose-dev.yml logs -fdocker compose -f docker-compose-dev.yml exec api shdocker compose -f docker-compose-dev.yml exec api alembic upgrade headdocker compose -f docker-compose-dev.yml exec api alembic revision --autogenerate -m "describe your change"docker compose -f docker-compose-dev.yml exec api pytest -qUse this if you want to run the WSL-specific Compose stack.
docker compose -f docker-compose-wsl.yml up -ddocker compose -f docker-compose-wsl.yml logs -fdocker compose -f docker-compose-wsl.yml downdocker compose -f docker-compose-wsl.yml down -vdocker compose -f docker-compose-wsl.yml exec api alembic upgrade headdocker compose -f docker-compose-wsl.yml exec api pytest -q-> Replace your own image name in the Compose file
This project uses separate Docker Compose files for different development and runtime workflows.
The full configuration should remain in the repository.
Use this file as the default local development setup.
It is intended for a workflow where:
- PostgreSQL runs in Docker
- the FastAPI application also runs in Docker
- the application is built from the local source tree
- source code is mounted into the container
- the API runs with auto-reload enabled for development
- PostgreSQL is exposed on host port
5434 - a dedicated Docker volume is used for development database persistence
This is the recommended option for everyday development when you want a fully containerized local environment.
Use this file for the WSL-based runtime setup.
It is intended for a workflow where:
- PostgreSQL runs in Docker
- the API runs from a prebuilt image instead of a local build
- the application is served with Gunicorn and Uvicorn workers
- PostgreSQL is exposed on host port
5433 - the API is exposed on host port
8001 - a separate Docker volume is used for database persistence in this environment
This makes it better suited for a more stable container-based runtime than the development Compose file.
When both services run inside Docker Compose, the API container should connect to PostgreSQL using the Compose service name and the container port:
DATABASE_HOSTNAME=postgres
DATABASE_PORT=5432Do not use localhost inside the API container, and do not use the published host port such as 5433 or 5434 for container-to-container communication.
The published host ports are only for access from your machine:
5434for PostgreSQL indocker-compose-dev.yml5433for PostgreSQL indocker-compose-wsl.yml
For example, if you run the FastAPI app locally instead of inside Docker, it should connect through the mapped host port, not the internal container port.
Keep the full Docker Compose definitions in docker-compose-dev.yml and docker-compose-wsl.yml, and use the README to explain:
- what each file is for
- which workflow is the default
- how services communicate inside Docker
- which host ports are exposed externally
This project uses pytest for automated testing.
pytest --disable-warnings -v -s -xThis is the main test command used in this project. It runs the full test suite with verbose output, shows standard output directly, suppresses warning noise, and stops on the first failure.
pytest -qUse this when you want a shorter, less verbose test output.
pytest tests/test_users.py -qpytest tests/test_users.py -vpytest -xUse a dedicated test database and enable testing mode before running the test suite.
With the current configuration, TESTING=true automatically makes the application use a database name with the _test suffix. In practice, that means:
- if
DATABASE_NAME=fastapi, tests will usefastapi_test - you should not usually set
DATABASE_NAME=fastapi_testtogether withTESTING=true, because that would producefastapi_test_test
A typical test configuration therefore looks like this:
DATABASE_NAME=fastapi
TESTING=trueThis helps keep test data isolated from your main development database.
The test setup uses pytest fixtures to:
- enable testing mode automatically
- create a separate SQLAlchemy session for each test
- override the FastAPI database dependency
- create and clean up database tables for each test function
- generate test users, authentication tokens, and sample post data
This keeps tests isolated and reduces the risk of one test affecting another.
The CI pipeline follows the same general pattern in GitHub Actions:
- start a PostgreSQL service
- set
TESTING=true - run Alembic migrations
- execute the test suite with pytest
This project uses Alembic to manage database schema changes.
alembic revision --autogenerate -m "describe your change"Use this after updating your SQLAlchemy models to generate a new migration script.
alembic upgrade headThis applies all pending migrations to the current database.
alembic downgrade -1Use this to undo the latest migration during development if needed.
alembic currentalembic historyAlembic should use the same configuration source as the application instead of duplicating database connection logic.
In this project, the recommended pattern is:
- use the normal application database URL for standard migrations
- allow an optional
DIRECT_DATABASE_URLoverride for environments that require a direct connection, such as certain hosted database providers (NEON) - keep the database URL resolution in
config.pyand let Alembic read from that centralized settings layer
In practice, alembic/env.py should set Alembic's sqlalchemy.url from settings.migration_database_url.
This keeps migration behavior aligned with the application and avoids maintaining separate connection logic in multiple places.
This setup helps keep migrations:
- consistent, because both the app and Alembic rely on the same settings source
- flexible, because direct connection overrides remain available when required
- easier to maintain, because connection string logic lives in one place
---
## 11. Build and publish to Docker Hub
### Build the image
```bash
docker image tag your-docker-image-name your-docker-hub-username/docker-hub-username:your-docker-image-name
docker logindocker push your-docker-hub-username/docker-hub-username:your-docker-image-namedocker build -t your-docker-hub-username/fastapi-backend:v1.0.0 .
docker push your-docker-hub-username/fastapi-backend:v1.0.0docker pull your-docker-hub-username/fastapi-backend:latest
docker run --env-file .env.docker -p 8000:8000 your-docker-hub-username/fastapi-backend:latestUse Neon as the hosted PostgreSQL database.
Recommended production variables:
DATABASE_HOSTNAME=your-neon-host
DATABASE_PORT=5432
DATABASE_NAME=your_db_name
DATABASE_USERNAME=your_user
DATABASE_PASSWORD=your_password
DATABASE_SSLMODE=require
DIRECT_DATABASE_URL=postgresql://.../neondb?sslmode=require&channel_binding=require
SECRET_KEY=replace_with_a_real_secret
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30If Render builds your app from the repository Dockerfile:
- Push your latest code to GitHub
- Create a new Web Service on Render
- Point Render to the repository
- Choose Docker-based deploy
- Set all environment variables in the Render dashboard
- Deploy
If Render pulls a prebuilt image:
- Build and push the Docker image to Docker Hub
- Create a new Web Service on Render
- Choose the option to deploy from an image/registry
- Set the image, for example:
yourdockerhubusername/fastapi-backend:latest
- Set all environment variables in the Render dashboard
- Deploy
If your deployment requires a command, use:
gunicorn -w 4 -k uvicorn.workers.UvicornWorker app.main:app --bind 0.0.0.0:$PORTStart Command Render runs this command to start your app with each deploy:
bash -c "python -m alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port $PORT"If you run migrations against Neon, DIRECT_DATABASE_URL should point to the direct connection string.
- WSL commands
List available WSL distributions:
wsl -l -vOpen Ubuntu 24.04 as root:
wsl -d Ubuntu-24.04 -u root- systemd service commands
Check service status:
sudo systemctl status api.serviceDisable and stop the service:
sudo systemctl disable --now api.serviceEnable and start the service:
sudo systemctl enable --now api.service- SSH key setup for GitHub Actions deploy
If you do not already have an SSH key, generate one on your local machine (Linux, macOS, or WSL):
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keygen -t ed25519 -C "github-actions-deploy"When prompted:
- Press Enter to use the default file path
- Press Enter for an empty passphrase
- Press Enter again to confirm
- Show your SSH keys
Private key:
cat ~/.ssh/id_ed25519Public key:
cat ~/.ssh/id_ed25519.pubUse them like this:
- Put the private key into the GitHub secret: PROD_SSH_PRIVATE_KEY
- Put the public key into the Ubuntu server file: ~/.ssh/authorized_keys
- Add the public key to the Ubuntu server
Step 1: Show your public key on your current machine
cat ~/.ssh/id_ed25519.pubIt will print one long line similar to this:
ssh-ed25519 github-actions-deploy
Copy the entire line.
Step 2: Connect to the Ubuntu server
ssh your_username@your_server_ipIf this is your own PC, open its terminal directly.
Step 3: Create the SSH folder and authorized_keys file on the Ubuntu server
mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keysWhat these commands do:
- mkdir -p ~/.ssh -> creates the .ssh folder if it does not exist
- chmod 700 ~/.ssh -> makes the folder private
- touch ~/.ssh/authorized_keys -> creates the authorized_keys file if it does not exist
- chmod 600 ~/.ssh/authorized_keys -> makes the file private
Step 4: Add the public key to authorized_keys
Open the file:
nano ~/.ssh/authorized_keysPaste the public key line you copied from:
cat ~/.ssh/id_ed25519.pubSave and exit:
- Ctrl + O
- Enter
- Ctrl + X
- Get the public IP from CLI
Option 1:
curl ifconfig.meOption 2:
curl https://api.ipify.orgThis project uses a self-hosted GitHub Actions runner for the Ubuntu / WSL deployment job.
How it works:
- GitHub matches the deploy job to a runner with the required labels
- the job runs directly on that runner machine
- the repository is checked out into the runner workspace
- deployment commands run locally on that same machine
- SSH is not required for deployment to the runner itself
If your workflow uses labels such as:
runs-on: [self-hosted, linux, x64, ubuntu-wsl]then the runner must have those labels to receive the job.
GitHub provides the exact commands during runner setup in:
Repository Settings > Actions > Runners > New self-hosted runner
For the current setup, the runner is being used from WSL and is currently located here:
/mnt/c/Users/user/actions-runnerA setup for this project would look like this:
cd /mnt/c/Users/user
mkdir -p actions-runner
cd actions-runner
curl -o actions-runner-linux-x64-2.333.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.333.0/actions-runner-linux-x64-2.333.0.tar.gz
echo "<published-sha256-checksum> actions-runner-linux-x64-2.333.0.tar.gz" | shasum -a 256 -c
tar xzf ./actions-runner-linux-x64-2.333.0.tar.gz
./config.sh --url https://github.com/ras-dec19/FastAPI-Project --token <generated-registration-token>
./run.sh./config.shregisters the runner with your repository./run.shstarts the runner manually in the current shell session- in this setup, running
cd actions-runnerworked because the runner folder is under/mnt/c/Users/user/, not under~/actions-runner - the download URL, version, and registration token are generated by GitHub during setup and may change over time
- right now, the runner is not installed as a service yet, which is why you have to start it manually with
./run.sh - if you want the runner to start automatically when the WSL Ubuntu environment starts, you can install it as a service after configuration
If you install the runner as a Linux service, you can manage it with:
cd /mnt/c/Users/user/actions-runner
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh statusYou only need to add the following if systemctl is not working in your WSL Ubuntu environment. If systemctl already works, you do not need to add this.
If systemctl is not working, fix WSL first:
sudo nano /etc/wsl.confPut in:
[boot]
systemd=trueThen from Windows PowerShell:
wsl.exe --shutdownFor this project, the important point is that the deploy job runs on the self-hosted runner machine itself, so no SSH step is needed for deployment to that same machine.
Make sure .gitignore includes at least:
venv/
__pycache__/
.pytest_cache/
.env
.env.*Make sure .dockerignore includes at least:
__pycache__/
venv/
.pytest_cache
.env
.env.*
.gitignore
Dockerfile*
docker-compose*.yml
gunicorn.service
nginx.conf
- Use
.env.localfor local Python commands. - Use
.env.dockerfor Docker containers. - Use
localhostwhen the Python process runs on your machine. - Use
postgreswhen the Python process runs inside Docker Compose. - Use
DIRECT_DATABASE_URLfor Neon migrations when available. - Prefer a separate test database for
pytest.