Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup Docker for self-hosting #45

Merged
merged 13 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
.vscode/
db.sqlite3
.env
53 changes: 53 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Stage 1: Base build stage
FROM python:3.10-slim AS builder

# Create the app directory
RUN mkdir /app

# Set the working directory
WORKDIR /app

# Set environment variables to optimize Python
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Install dependencies first for caching benefit
RUN pip install --upgrade pip
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt

# Stage 2: Production stage
FROM python:3.10-slim

RUN useradd -m -r appuser && \
mkdir /app && \
chown -R appuser /app

# Copy the Python dependencies from the builder stage
COPY --from=builder /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/
COPY --from=builder /usr/local/bin/ /usr/local/bin/

# Set the working directory
WORKDIR /app

# Copy application code
COPY --chown=appuser:appuser . .

# Set environment variables to optimize Python
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Create the staticfiles directory
RUN mkdir -p /app/staticfiles && chown appuser:appuser /app/staticfiles

# Switch to non-root user
USER appuser

# Expose the application port
EXPOSE 8000

# Make entry file executable
RUN chmod +x /app/entrypoint.prod.sh

# Start the application using Gunicorn
CMD ["/app/entrypoint.prod.sh"]
33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<div align="center">
<h1>
<img src="static/images/logo.png" width="400" alt="Logo Icon"/>
<img src="passmanager/static/images/logo.png" width="400" alt="Logo Icon"/>
</h1>
<p>Self-hosted password manager for secure online credentials.<br>Written in Python/Django</p>
<a href="https://github.com/KafetzisThomas/PassManagerWeb/actions/workflows/tests.yml">
<img src = "https://github.com/KafetzisThomas/PassManagerWeb/actions/workflows/tests.yml/badge.svg" alt = 'Run Tests'/>
<img src="https://github.com/KafetzisThomas/PassManagerWeb/actions/workflows/tests.yml/badge.svg" alt="Run Tests"/>
</a>
<img src="https://img.shields.io/badge/Docker-Enabled-blue?logo=docker" alt="Docker Enabled"/>
</div>

---
Expand Down Expand Up @@ -52,14 +53,28 @@ $ nano main/.env

Add the following environment variables (modify as needed):
```bash
# Django settings
➜ SECRET_KEY="example_secret_key" # https://stackoverflow.com/a/57678930
➜ ALLOWED_HOSTS="localhost"
➜ CSRF_TRUSTED_ORIGINS="http://localhost:8001"
➜ DEBUG=True # For development

# OPTIONAL: PostgreSQL Configuration (remote production)
➜ DATABASE_URL="postgres://[username]:[password]@[host]:[port]/[db_name]"

# Email settings
➜ EMAIL_HOST_USER="example_email_host"
➜ EMAIL_HOST_PASSWORD="example_email_password"
```

Save changes and close the file.

> **Note:** You can deploy the application using Docker:
> **NGINX + Gunicorn + External DB**
> ```sh
> $ docker compose up
> ```

### Migrate Database

```bash
Expand All @@ -84,19 +99,19 @@ $ python3 manage.py test

<div align = 'center'>
<h2>Vault</h2>
<img src='static/images/vault_page.png' alt='Vault'>
<img src='passmanager/static/images/vault_page.png' alt='Vault'>
<br><h2>Password Generator</h2>
<img src='static/images/password_generator_page.png' alt='Password Generator'>
<img src='passmanager/static/images/password_generator_page.png' alt='Password Generator'>
<br><h2>Import Data</h2>
<img src='static/images/import_data_page.png' alt='Import Data'>
<img src='passmanager/static/images/import_data_page.png' alt='Import Data'>
<br><h2>Password Checkup</h2>
<img src = 'static/images/password_checkup_page.png' alt='Password Checkup'>
<img src = 'passmanager/static/images/password_checkup_page.png' alt='Password Checkup'>
<br><h2>Account Settings</h2>
<img src='static/images/account_page.png' alt='Account Settings'>
<img src='passmanager/static/images/account_page.png' alt='Account Settings'>
<br><h2>New Item</h2>
<img src='static/images/new_item_page.png' alt='New Item'>
<img src='passmanager/static/images/new_item_page.png' alt='New Item'>
<br><h2>Edit Item</h2>
<img src='static/images/edit_item_page.png' alt='Edit Item'><br>
<img src='passmanager/static/images/edit_item_page.png' alt='Edit Item'><br>
</div>

## Contributing Guidelines
Expand Down
12 changes: 0 additions & 12 deletions build.sh

This file was deleted.

15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
app:
build: .
container_name: django-docker
env_file:
- main/.env

frontend-proxy:
image: nginx:latest
ports:
- "8001:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
13 changes: 13 additions & 0 deletions entrypoint.prod.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

# Exit on error
set -o errexit

# Collect static files
python manage.py collectstatic --noinput

# Apply database migrations
python manage.py migrate --noinput

# Start gunicorn
python -m gunicorn --bind 0.0.0.0:8000 --workers 3 main.wsgi:application
21 changes: 5 additions & 16 deletions main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
SECRET_KEY = os.getenv("SECRET_KEY")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv("DEBUG", "").lower() == "true"
DEBUG = os.environ.get("DEBUG") == "True"

ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "127.0.0.1").split(",")
CSRF_TRUSTED_ORIGINS = os.getenv("CSRF_TRUSTED_ORIGINS", "https://127.0.0.1").split(",")

# Application definition

Expand All @@ -54,7 +55,6 @@

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
Expand Down Expand Up @@ -100,7 +100,7 @@
}
}
else:
# Production using PostgreSQL
# Production using PostgreSQL (remote)
DATABASES = {
"default": dj_database_url.parse(os.getenv("DATABASE_URL")),
}
Expand Down Expand Up @@ -139,19 +139,8 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/

STATICFILES_DIRS = [
BASE_DIR / "static",
]

STATIC_URL = "static/"

# This production code might break development mode, so we check whether we're in DEBUG mode
if not DEBUG:
# Tell Django to copy static assets into a path called `staticfiles` (this is specific to Render)
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
# Enable the WhiteNoise storage backend, which compresses static files to reduce disk use
# and renames the files with unique names for each version to support long-term caching
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STATIC_ROOT = BASE_DIR / "staticfiles"

# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
Expand Down
41 changes: 41 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Sets the max number of simultaneous connections that can be opened by a worker process
events {
worker_connections 1024;
}

http {
# Enable gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

server {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
listen 80;

# Requests to /static/ are served directly from the /static/ directory
location /static/ {
alias /app/staticfiles/;
expires 7d;
}

# Configuration for serving media files
# location /media/ {
# alias /home/app/web/mediafiles/;
# }

# Handles all other requests
location / {
# Forward requests to Django application
proxy_pass http://app:8000;

# Pass important headers to Django for proper request handling
proxy_set_header Host $host; # Original host header
proxy_set_header X-Real-IP $remote_addr; # Client's real IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Chain of IP addresses
proxy_set_header X-Forwarded-Proto $scheme; # Original protocol (http/https)
}
}
}
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,3 @@ soupsieve==2.5
sqlparse==0.5.0
typing_extensions==4.12.2
uvicorn==0.30.1
whitenoise==6.7.0