Fix Supervisor start error in containerized environment
- Enhanced database connection with multiple fallback paths - Updated configuration to use environment variables - Fixed Alembic migrations to work with dynamic database path - Improved robustness of database initialization - Added Docker deployment instructions to README - Updated environment variables documentation
This commit is contained in:
parent
84555ef4e8
commit
d024248fe9
34
README.md
34
README.md
@ -104,6 +104,30 @@ uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
|||||||
|
|
||||||
The API will be available at [http://localhost:8000](http://localhost:8000)
|
The API will be available at [http://localhost:8000](http://localhost:8000)
|
||||||
|
|
||||||
|
### Running with Docker
|
||||||
|
|
||||||
|
You can also run the application using Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the Docker image
|
||||||
|
docker build -t hrplatform-backend .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 8000:8000 -e DATABASE_PATH=/app/storage/db/db.sqlite hrplatform-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
For production deployments, make sure to set the necessary environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8000:8000 \
|
||||||
|
-e SECRET_KEY="your-secure-key" \
|
||||||
|
-e FIRST_SUPERUSER="admin@yourdomain.com" \
|
||||||
|
-e FIRST_SUPERUSER_PASSWORD="secure-password" \
|
||||||
|
-e DATABASE_PATH="/app/storage/db/db.sqlite" \
|
||||||
|
-v /host/path/to/data:/app/storage \
|
||||||
|
hrplatform-backend
|
||||||
|
```
|
||||||
|
|
||||||
## API Documentation
|
## API Documentation
|
||||||
|
|
||||||
Once the server is running, you can access the interactive API documentation:
|
Once the server is running, you can access the interactive API documentation:
|
||||||
@ -168,7 +192,15 @@ Once the server is running, you can access the interactive API documentation:
|
|||||||
| ACCESS_TOKEN_EXPIRE_MINUTES| JWT token expiration time in minutes | 11520 (8 days) |
|
| ACCESS_TOKEN_EXPIRE_MINUTES| JWT token expiration time in minutes | 11520 (8 days) |
|
||||||
| FIRST_SUPERUSER | Email for the initial admin user | admin@example.com |
|
| FIRST_SUPERUSER | Email for the initial admin user | admin@example.com |
|
||||||
| FIRST_SUPERUSER_PASSWORD | Password for the initial admin user | admin123 |
|
| FIRST_SUPERUSER_PASSWORD | Password for the initial admin user | admin123 |
|
||||||
| SQLALCHEMY_DATABASE_URI | Database connection URI | sqlite:////app/storage/db/db.sqlite |
|
| DATABASE_PATH | Path to the SQLite database file | Auto-detected with fallbacks |
|
||||||
|
|
||||||
|
The application will automatically try the following paths for the database (in order):
|
||||||
|
1. Path specified in the DATABASE_PATH environment variable (if set)
|
||||||
|
2. /app/storage/db/db.sqlite (standard Docker container path)
|
||||||
|
3. /tmp/hrplatform/db/db.sqlite (fallback for limited permissions)
|
||||||
|
4. ./db/db.sqlite (relative to the current working directory)
|
||||||
|
|
||||||
|
This makes the application work reliably in different environments, including Docker containers, without requiring manual configuration.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -35,8 +35,8 @@ script_location = migrations
|
|||||||
# are written from script.py.mako
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
# SQLite URL example
|
# SQLite URL example - this will be overridden in env.py
|
||||||
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
|
sqlalchemy.url = sqlite:///db/db.sqlite
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
# post_write_hooks defines scripts or Python functions that are run
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
from pydantic import validator, EmailStr
|
from pydantic import EmailStr
|
||||||
import secrets
|
import secrets
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
@ -9,29 +10,23 @@ class Settings(BaseSettings):
|
|||||||
API_V1_STR: str = "/api/v1"
|
API_V1_STR: str = "/api/v1"
|
||||||
|
|
||||||
# SECURITY
|
# SECURITY
|
||||||
SECRET_KEY: str = secrets.token_urlsafe(32)
|
SECRET_KEY: str = os.environ.get("SECRET_KEY", secrets.token_urlsafe(32))
|
||||||
# 60 minutes * 24 hours * 8 days = 8 days
|
# 60 minutes * 24 hours * 8 days = 8 days
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", 60 * 24 * 8))
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
BACKEND_CORS_ORIGINS: List[str] = ["*"]
|
BACKEND_CORS_ORIGINS: List[str] = ["*"]
|
||||||
|
|
||||||
# First superuser
|
# First superuser
|
||||||
FIRST_SUPERUSER: EmailStr = "admin@example.com"
|
FIRST_SUPERUSER: EmailStr = os.environ.get("FIRST_SUPERUSER", "admin@example.com")
|
||||||
FIRST_SUPERUSER_PASSWORD: str = "admin123"
|
FIRST_SUPERUSER_PASSWORD: str = os.environ.get("FIRST_SUPERUSER_PASSWORD", "admin123")
|
||||||
|
|
||||||
# SQLITE DB
|
# Database path - this is just a placeholder, the actual path is determined in db/session.py
|
||||||
|
DATABASE_PATH: Optional[str] = os.environ.get("DATABASE_PATH")
|
||||||
|
|
||||||
|
# SQLITE DB URI - will be set by db/session.py
|
||||||
SQLALCHEMY_DATABASE_URI: Optional[str] = None
|
SQLALCHEMY_DATABASE_URI: Optional[str] = None
|
||||||
|
|
||||||
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
|
|
||||||
def assemble_db_connection(cls, v: Optional[str], values: dict) -> str:
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
from pathlib import Path
|
|
||||||
DB_DIR = Path("/app") / "storage" / "db"
|
|
||||||
DB_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
return f"sqlite:///{DB_DIR}/db.sqlite"
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
case_sensitive = True
|
case_sensitive = True
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
|
@ -1,13 +1,44 @@
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Try to get database path from environment variable
|
||||||
|
DB_PATH = os.environ.get("DATABASE_PATH")
|
||||||
|
|
||||||
DB_DIR = Path("/app") / "storage" / "db"
|
if DB_PATH:
|
||||||
DB_DIR.mkdir(parents=True, exist_ok=True)
|
# Use the provided path
|
||||||
|
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}"
|
||||||
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite"
|
else:
|
||||||
|
# Default paths with fallbacks for different environments
|
||||||
|
possible_paths = [
|
||||||
|
Path("/app/storage/db/db.sqlite"), # Docker container standard path
|
||||||
|
Path("/tmp/hrplatform/db/db.sqlite"), # Fallback to tmp directory
|
||||||
|
Path.cwd() / "db" / "db.sqlite" # Local development in current directory
|
||||||
|
]
|
||||||
|
|
||||||
|
# Find the first parent directory that is writable
|
||||||
|
for path in possible_paths:
|
||||||
|
try:
|
||||||
|
# Ensure directory exists
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Test if we can write to this directory
|
||||||
|
test_file = path.parent / ".write_test"
|
||||||
|
test_file.touch()
|
||||||
|
test_file.unlink()
|
||||||
|
SQLALCHEMY_DATABASE_URL = f"sqlite:///{path}"
|
||||||
|
print(f"Using database at: {path}")
|
||||||
|
break
|
||||||
|
except (PermissionError, OSError):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# If we get here, none of the paths worked
|
||||||
|
raise RuntimeError(
|
||||||
|
"Could not find a writable location for the database. "
|
||||||
|
"Please set the DATABASE_PATH environment variable."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the engine with the configured URL
|
||||||
engine = create_engine(
|
engine = create_engine(
|
||||||
SQLALCHEMY_DATABASE_URL,
|
SQLALCHEMY_DATABASE_URL,
|
||||||
connect_args={"check_same_thread": False}
|
connect_args={"check_same_thread": False}
|
||||||
|
@ -16,6 +16,12 @@ sys.path.append(str(BASE_DIR))
|
|||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
|
# Import database configuration to get the right database URL
|
||||||
|
from app.db.session import SQLALCHEMY_DATABASE_URL # noqa: E402
|
||||||
|
|
||||||
|
# Override the SQLAlchemy URL with our dynamically determined one
|
||||||
|
config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL)
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
# Interpret the config file for Python logging.
|
||||||
# This line sets up loggers basically.
|
# This line sets up loggers basically.
|
||||||
if config.config_file_name is not None:
|
if config.config_file_name is not None:
|
||||||
@ -25,7 +31,7 @@ if config.config_file_name is not None:
|
|||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
# from myapp import mymodel
|
# from myapp import mymodel
|
||||||
# target_metadata = mymodel.Base.metadata
|
# target_metadata = mymodel.Base.metadata
|
||||||
from app.db.base import Base # noqa
|
from app.db.base import Base # noqa: E402
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
@ -9,6 +9,7 @@ Usage:
|
|||||||
python run_migrations.py --help
|
python run_migrations.py --help
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -17,9 +18,40 @@ BASE_DIR = Path(__file__).resolve().parent
|
|||||||
sys.path.append(str(BASE_DIR))
|
sys.path.append(str(BASE_DIR))
|
||||||
print(f"Added {BASE_DIR} to Python path")
|
print(f"Added {BASE_DIR} to Python path")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
# Set environment variables for containerized environments if not already set
|
||||||
# Import alembic's main function
|
if "DATABASE_PATH" not in os.environ:
|
||||||
from alembic.config import main
|
# Try multiple possible paths for the database
|
||||||
|
possible_paths = [
|
||||||
|
"/app/storage/db/db.sqlite",
|
||||||
|
"/tmp/hrplatform/db/db.sqlite",
|
||||||
|
str(BASE_DIR / "db" / "db.sqlite")
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in possible_paths:
|
||||||
|
try:
|
||||||
|
# Ensure directory exists
|
||||||
|
db_dir = Path(path).parent
|
||||||
|
db_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Test if we can write to this directory
|
||||||
|
test_file = db_dir / ".write_test"
|
||||||
|
test_file.touch()
|
||||||
|
test_file.unlink()
|
||||||
|
os.environ["DATABASE_PATH"] = path
|
||||||
|
print(f"Using database at: {path}")
|
||||||
|
break
|
||||||
|
except (PermissionError, OSError):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("WARNING: Could not find a writable location for the database.")
|
||||||
|
print("Please set the DATABASE_PATH environment variable.")
|
||||||
|
|
||||||
# Execute alembic command with sys.argv (e.g., 'upgrade', 'head')
|
if __name__ == "__main__":
|
||||||
main(argv=sys.argv[1:])
|
try:
|
||||||
|
# Import alembic's main function
|
||||||
|
from alembic.config import main
|
||||||
|
|
||||||
|
# Execute alembic command with sys.argv (e.g., 'upgrade', 'head')
|
||||||
|
main(argv=sys.argv[1:])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error running migrations: {e}")
|
||||||
|
sys.exit(1)
|
Loading…
x
Reference in New Issue
Block a user