TaskFlow is a FastAPI backend for a Flutter task-management app. It provides JWT authentication, refresh tokens, user-specific task CRUD endpoints, PostgreSQL storage, Docker deployment, Swagger docs, and basic tests.
This README is written for someone who wants to clone the project and host it on a Webmin or Virtualmin server.
- User registration and login
- JWT access tokens and refresh tokens
- Password hashing
- Protected task APIs
- PostgreSQL with SQLAlchemy
- Dockerfile and Docker Compose
- Persistent Docker volumes
- Swagger and ReDoc documentation
- Health-check endpoint
- Pytest test setup
- Python 3.12 in Docker
- FastAPI
- SQLAlchemy
- PostgreSQL 16
- Psycopg 3
- Docker Compose
- Apache or Nginx reverse proxy
- Webmin or Virtualmin for server management
taskflow_fastapi/
├── app/
│ ├── api/
│ ├── core/
│ ├── db/
│ ├── models/
│ ├── schemas/
│ └── main.py
├── tests/
├── uploads/
├── .env.example
├── compose.yaml
├── Dockerfile
├── requirements.txt
└── README.md
| Method | Endpoint | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/health |
No | Health check |
| POST | /api/v1/auth/register |
No | Register user |
| POST | /api/v1/auth/login |
No | Login and receive tokens |
| POST | /api/v1/auth/refresh |
Refresh token | Receive new tokens |
| GET | /api/v1/tasks |
Access token | List current user's tasks |
| POST | /api/v1/tasks |
Access token | Create task |
| GET | /api/v1/tasks/{id} |
Access token | Get one task |
| PATCH | /api/v1/tasks/{id} |
Access token | Update task |
| DELETE | /api/v1/tasks/{id} |
Access token | Delete task |
Use the local server URL while testing on the server:
BASE_URL=http://127.0.0.1:8000/api/v1Use the public HTTPS URL after deployment:
BASE_URL=https://api-taskflow.example.com/api/v1curl "$BASE_URL/health"Expected response:
{"status":"healthy"}curl -X POST "$BASE_URL/auth/register" \
-H "Content-Type: application/json" \
-d '{
"name": "Test User",
"email": "test@example.com",
"password": "password123"
}'curl -X POST "$BASE_URL/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123"
}'Example response:
{
"access_token": "ACCESS_TOKEN_HERE",
"refresh_token": "REFRESH_TOKEN_HERE",
"token_type": "bearer"
}Save the access token for protected requests:
ACCESS_TOKEN=ACCESS_TOKEN_HEREcurl -X POST "$BASE_URL/tasks" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"title": "Learn FastAPI",
"description": "Practice authentication and CRUD",
"priority": "high",
"is_completed": false,
"due_date": "2026-06-25T18:00:00Z"
}'curl "$BASE_URL/tasks" \
-H "Authorization: Bearer $ACCESS_TOKEN"Replace 1 with the task ID:
curl "$BASE_URL/tasks/1" \
-H "Authorization: Bearer $ACCESS_TOKEN"Use PATCH to send only the fields you want to change:
curl -X PATCH "$BASE_URL/tasks/1" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"title": "Learn FastAPI and PostgreSQL",
"is_completed": true
}'curl -X DELETE "$BASE_URL/tasks/1" \
-H "Authorization: Bearer $ACCESS_TOKEN"Successful delete returns HTTP 204 No Content.
curl -X POST "$BASE_URL/auth/refresh" \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "REFRESH_TOKEN_HERE"
}'Production traffic should flow like this:
Flutter app or browser
|
| HTTPS
v
api.your-domain.com
|
| Apache/Nginx reverse proxy
v
127.0.0.1:8000
|
v
FastAPI Docker container
|
v
PostgreSQL Docker container
The API container is bound to 127.0.0.1:8000, so it is reachable only from the server itself. Apache or Nginx handles the public HTTPS traffic.
Your Webmin/Virtualmin server needs:
- SSH access
sudoor root access- Git
- Docker
- Docker Compose v2
- Apache or Nginx
- A domain or subdomain
- Ports
80and443open - SSL certificate support, usually Let's Encrypt
Check the server:
cat /etc/os-release
git --version
docker --version
docker compose versionUse a subdomain for the API, for example:
api-taskflow.example.com
Create a DNS A record:
api-taskflow.example.com -> YOUR_SERVER_PUBLIC_IP
In Virtualmin, you can create a virtual server:
Virtualmin -> Create Virtual Server
Or create a sub-server/subdomain under an existing site. DNS can take a few minutes to several hours to propagate.
SSH into the server:
ssh username@YOUR_SERVER_IPChoose a deployment location. These are common choices:
cd /home/usernameClone the repository:
git clone https://github.com/YOUR_USERNAME/taskflow_fastapi.git
cd taskflow_fastapiIf your repository is private, make sure the server has access through an SSH key or GitHub token.
Copy the example file:
cp .env.example .envGenerate a secure secret key:
openssl rand -hex 64Edit the environment file:
nano .envExample production .env:
APP_NAME=TaskFlow API
API_V1_PREFIX=/api/v1
POSTGRES_DB=taskflow
POSTGRES_USER=taskflow
POSTGRES_PASSWORD=replace_with_a_strong_database_password
DATABASE_URL=postgresql+psycopg://taskflow:replace_with_a_strong_database_password@db:5432/taskflow
SECRET_KEY=replace_with_the_generated_secret_key
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=7
CORS_ORIGINS=["https://api-taskflow.example.com"]Important:
POSTGRES_PASSWORDmust match the password insideDATABASE_URL.- In Docker Compose, the database hostname must be
db. - Do not commit
.envto Git. - Replace
api-taskflow.example.comwith your real API domain.
Save in Nano:
Ctrl + O
Enter
Ctrl + X
The included compose.yaml starts two services:
db: PostgreSQLapi: FastAPI
The important production setting is:
ports:
- "127.0.0.1:8000:8000"This keeps FastAPI private to the server. Your public domain should reach it through Apache or Nginx.
PostgreSQL data is stored in:
postgres_data
Uploaded files are stored in:
taskflow_uploads
Run:
docker compose up --build -dCheck containers:
docker compose psYou should see both services running:
taskflow_db
taskflow_api
Check API logs:
docker compose logs -f apiCheck PostgreSQL:
docker compose exec db sh -lc 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"'Test the API on the server:
curl http://127.0.0.1:8000/api/v1/healthExpected response:
{"status":"healthy"}Use this step to connect your public domain to the private FastAPI port.
Enable required Apache modules on Debian or Ubuntu:
sudo a2enmod proxy proxy_http headers ssl
sudo systemctl restart apache2In Virtualmin, open the virtual server for your API domain and edit the Apache virtual host, or add a custom proxy configuration.
HTTP virtual host example:
<VirtualHost *:80>
ServerName api-taskflow.example.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
</VirtualHost>HTTPS virtual host example:
<VirtualHost *:443>
ServerName api-taskflow.example.com
SSLEngine on
SSLCertificateFile /path/to/fullchain.pem
SSLCertificateKeyFile /path/to/privkey.pem
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
</VirtualHost>Check and reload Apache:
sudo apachectl configtest
sudo systemctl reload apache2On AlmaLinux, Rocky Linux, or CentOS, Apache may be named httpd:
sudo systemctl reload httpdIf the server uses Nginx instead of Apache:
server {
listen 80;
server_name api-taskflow.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Check and reload Nginx:
sudo nginx -t
sudo systemctl reload nginxIn Virtualmin:
Virtualmin -> Select API domain -> Server Configuration -> SSL Certificate -> Let's Encrypt
Before requesting SSL, confirm:
- DNS points to the server.
- Port
80is open. - Apache or Nginx is running.
- The API domain has a virtual server or proxy config.
After SSL is enabled, test:
https://api-taskflow.example.com/api/v1/health
https://api-taskflow.example.com/docs
https://api-taskflow.example.com/redoc
Public ports:
80/tcp
443/tcp
Do not expose port 8000 publicly.
For UFW:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw statusFor Firewalld:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reloadOpen:
https://api-taskflow.example.com/docs
Register:
{
"name": "Test User",
"email": "test@example.com",
"password": "password123"
}Login with the same email and password. Copy the access_token, click Authorize, and paste the token value.
For normal HTTP clients, send:
Authorization: Bearer YOUR_ACCESS_TOKEN
Then test:
POST /api/v1/tasks
GET /api/v1/tasks
Set the production API base URL:
class ApiConstants {
static const String baseUrl =
'https://api-taskflow.example.com/api/v1';
}Send protected requests with:
Authorization: Bearer ACCESS_TOKEN
Store tokens securely in the Flutter app, for example with flutter_secure_storage.
SSH into the server:
ssh username@YOUR_SERVER_IP
cd /home/username/taskflow_fastapiPull the latest code:
git pullRebuild and restart:
docker compose down
docker compose up --build -dCheck health:
curl http://127.0.0.1:8000/api/v1/healthdocker compose ps
docker compose logs -f api
docker compose logs -f db
docker compose restart
docker compose down
docker compose up -dOpen PostgreSQL shell:
docker compose exec db sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'Exit PostgreSQL shell:
\q
Delete all containers and volumes:
docker compose down -vWarning: docker compose down -v deletes the PostgreSQL database volume and uploaded-file volume.
Create a PostgreSQL backup:
mkdir -p backups
docker compose exec -T db sh -lc 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backups/taskflow_$(date +%F).sqlRestore a backup:
docker compose exec -T db sh -lc 'psql -U "$POSTGRES_USER" "$POSTGRES_DB"' < backups/taskflow_YYYY-MM-DD.sqlInspect volumes:
docker volume ls
docker volume inspect taskflow_fastapi_postgres_dataInside Docker:
docker compose exec api pytestLocally:
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pytestUse Python 3.12 or 3.13 for local testing with the pinned dependencies. The Docker image uses Python 3.12.
Add the user to the Docker group:
sudo usermod -aG docker $USERLog out and log in again.
Check:
docker compose ps
docker compose logs apiCheck:
docker compose logs db
docker compose exec db sh -lc 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"'Make sure POSTGRES_PASSWORD and DATABASE_URL use the same password.
Check:
sudo ss -lntp | grep 8000Change the host port in compose.yaml:
ports:
- "127.0.0.1:8001:8000"Then update the reverse proxy to:
http://127.0.0.1:8001
Check FastAPI locally:
curl http://127.0.0.1:8000/api/v1/health
docker compose ps
docker compose logs apiThen check Apache or Nginx logs.
Confirm:
- DNS points to the correct server IP.
- Port
80is reachable. - Apache or Nginx is running.
- No duplicate virtual host is using the same domain.
- Always use HTTPS.
- Keep
.envprivate. - Use strong passwords and a strong
SECRET_KEY. - Keep Docker and the server OS updated.
- Back up the database before updates.
- Do not run
docker compose down -vunless data deletion is intended. - Add Alembic before changing production database schemas.
- Add refresh-token revocation and rate limiting before serious public use.
[ ] Domain or subdomain points to the server
[ ] Git repository cloned on the server
[ ] .env created and edited
[ ] SECRET_KEY generated
[ ] PostgreSQL password set in POSTGRES_PASSWORD and DATABASE_URL
[ ] Docker Compose app starts successfully
[ ] Local health endpoint works on 127.0.0.1:8000
[ ] Apache or Nginx reverse proxy configured
[ ] HTTPS certificate installed
[ ] Public health endpoint works
[ ] Swagger opens
[ ] Flutter base URL updated
[ ] Backup command tested