(config) Harden Docker deployment for remote servers

Run the Django control panel with Gunicorn instead of the development
runserver and serve static files through WhiteNoise.

Add restart policies, healthchecks, .env-driven production settings, and
a sample .env file for single-server deployments. Update the Docker
entrypoint to collect static assets and document the remote server
deployment and update flow in the README.
This commit is contained in:
2026-05-19 15:33:09 +02:00
parent c018011e83
commit 1297a839d4
7 changed files with 113 additions and 18 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
POBSYNC_BACKUP_ROOT=/mnt/backups/pobsync
POBSYNC_DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,backup.example.com
POBSYNC_DJANGO_SECRET_KEY=change-me-to-a-long-random-secret
POBSYNC_DJANGO_DEBUG=0
POBSYNC_WEB_BIND=127.0.0.1
POBSYNC_GUNICORN_WORKERS=2
POBSYNC_GUNICORN_TIMEOUT=120

View File

@@ -24,4 +24,4 @@ RUN chmod +x ./scripts/docker-entrypoint
EXPOSE 8000 EXPOSE 8000
ENTRYPOINT ["./scripts/docker-entrypoint"] ENTRYPOINT ["./scripts/docker-entrypoint"]
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] CMD ["gunicorn", "pobsync_server.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "120"]

View File

@@ -128,7 +128,8 @@ Run the scheduler alongside the web admin:
docker compose up --build web scheduler worker docker compose up --build web scheduler worker
``` ```
The container persists `/opt/pobsync` and the SQLite database in Docker volumes. The web service runs Django through Gunicorn and serves static files with WhiteNoise. The container persists `/opt/pobsync`
and the SQLite database in Docker volumes.
Backup data is always available at `/backups` inside the containers. By default this uses `./backups` on the host. Backup data is always available at `/backups` inside the containers. By default this uses `./backups` on the host.
Override the host-side mount with `POBSYNC_BACKUP_ROOT`: Override the host-side mount with `POBSYNC_BACKUP_ROOT`:
@@ -139,6 +140,47 @@ POBSYNC_BACKUP_ROOT=/mnt/backups/pobsync docker compose up --build web scheduler
The Django setup UI keeps the backup root fixed at `/backups`; only the Docker mount decides which host directory The Django setup UI keeps the backup root fixed at `/backups`; only the Docker mount decides which host directory
that points to. that points to.
## Remote Server Deployment
For a single backup server, use Docker Compose with the SQLite services and put a reverse proxy such as Caddy, nginx,
or Traefik in front of `web`.
Create a `.env` from the example:
```
cp .env.example .env
```
Set at least:
```
POBSYNC_BACKUP_ROOT=/mnt/backups/pobsync
POBSYNC_DJANGO_ALLOWED_HOSTS=backup.example.com,localhost,127.0.0.1
POBSYNC_DJANGO_SECRET_KEY=<long-random-secret>
POBSYNC_DJANGO_DEBUG=0
POBSYNC_WEB_BIND=127.0.0.1
```
Deploy or update:
```
git pull
docker compose build web scheduler worker
docker compose up -d --force-recreate web scheduler worker
docker compose exec web python manage.py migrate
```
Check service state:
```
docker compose ps
docker compose logs --tail=100 worker
docker compose logs --tail=100 scheduler
```
`web`, `scheduler`, and `worker` use `restart: unless-stopped` and Docker healthchecks. If `POBSYNC_WEB_BIND` is
`127.0.0.1`, expose the app through your reverse proxy instead of directly publishing it to the internet.
## Django-Managed SSH Keys ## Django-Managed SSH Keys
SSH keys can be managed from the Django UI at `/ssh-credentials/`. Add a private key there, optionally paste SSH keys can be managed from the Django UI at `/ssh-credentials/`. Add a private key there, optionally paste

View File

@@ -1,26 +1,33 @@
services: services:
web: web:
build: . build: .
command: python manage.py runserver 0.0.0.0:8000 command: gunicorn pobsync_server.wsgi:application --bind 0.0.0.0:8000 --workers ${POBSYNC_GUNICORN_WORKERS:-2} --timeout ${POBSYNC_GUNICORN_TIMEOUT:-120}
restart: unless-stopped
environment: environment:
POBSYNC_DJANGO_DEBUG: "1" POBSYNC_DJANGO_DEBUG: "${POBSYNC_DJANGO_DEBUG:-0}"
POBSYNC_DJANGO_SECRET_KEY: "dev-only-change-me" POBSYNC_DJANGO_SECRET_KEY: "${POBSYNC_DJANGO_SECRET_KEY:-dev-only-change-me}"
POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}" POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}"
POBSYNC_HOME: "/opt/pobsync" POBSYNC_HOME: "/opt/pobsync"
POBSYNC_SQLITE_PATH: "/var/lib/pobsync/pobsync.sqlite3" POBSYNC_SQLITE_PATH: "/var/lib/pobsync/pobsync.sqlite3"
ports: ports:
- "8010:8000" - "${POBSYNC_WEB_BIND:-0.0.0.0}:8010:8000"
volumes: volumes:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- pobsync_db:/var/lib/pobsync - pobsync_db:/var/lib/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 30s
timeout: 10s
retries: 3
scheduler: scheduler:
build: . build: .
command: python manage.py run_pobsync_scheduler --loop --interval 60 command: python manage.py run_pobsync_scheduler --loop --interval 60
restart: unless-stopped
environment: environment:
POBSYNC_DJANGO_DEBUG: "1" POBSYNC_DJANGO_DEBUG: "${POBSYNC_DJANGO_DEBUG:-0}"
POBSYNC_DJANGO_SECRET_KEY: "dev-only-change-me" POBSYNC_DJANGO_SECRET_KEY: "${POBSYNC_DJANGO_SECRET_KEY:-dev-only-change-me}"
POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}" POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}"
POBSYNC_HOME: "/opt/pobsync" POBSYNC_HOME: "/opt/pobsync"
POBSYNC_SQLITE_PATH: "/var/lib/pobsync/pobsync.sqlite3" POBSYNC_SQLITE_PATH: "/var/lib/pobsync/pobsync.sqlite3"
@@ -28,13 +35,19 @@ services:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- pobsync_db:/var/lib/pobsync - pobsync_db:/var/lib/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 30s
timeout: 10s
retries: 3
worker: worker:
build: . build: .
command: python manage.py run_pobsync_worker --loop --interval 15 command: python manage.py run_pobsync_worker --loop --interval 15
restart: unless-stopped
environment: environment:
POBSYNC_DJANGO_DEBUG: "1" POBSYNC_DJANGO_DEBUG: "${POBSYNC_DJANGO_DEBUG:-0}"
POBSYNC_DJANGO_SECRET_KEY: "dev-only-change-me" POBSYNC_DJANGO_SECRET_KEY: "${POBSYNC_DJANGO_SECRET_KEY:-dev-only-change-me}"
POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}" POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}"
POBSYNC_HOME: "/opt/pobsync" POBSYNC_HOME: "/opt/pobsync"
POBSYNC_SQLITE_PATH: "/var/lib/pobsync/pobsync.sqlite3" POBSYNC_SQLITE_PATH: "/var/lib/pobsync/pobsync.sqlite3"
@@ -42,14 +55,20 @@ services:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- pobsync_db:/var/lib/pobsync - pobsync_db:/var/lib/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 30s
timeout: 10s
retries: 3
web-mariadb: web-mariadb:
profiles: ["mariadb"] profiles: ["mariadb"]
build: . build: .
command: python manage.py runserver 0.0.0.0:8000 command: gunicorn pobsync_server.wsgi:application --bind 0.0.0.0:8000 --workers ${POBSYNC_GUNICORN_WORKERS:-2} --timeout ${POBSYNC_GUNICORN_TIMEOUT:-120}
restart: unless-stopped
environment: environment:
POBSYNC_DJANGO_DEBUG: "1" POBSYNC_DJANGO_DEBUG: "${POBSYNC_DJANGO_DEBUG:-0}"
POBSYNC_DJANGO_SECRET_KEY: "dev-only-change-me" POBSYNC_DJANGO_SECRET_KEY: "${POBSYNC_DJANGO_SECRET_KEY:-dev-only-change-me}"
POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}" POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}"
POBSYNC_HOME: "/opt/pobsync" POBSYNC_HOME: "/opt/pobsync"
POBSYNC_DB_ENGINE: "mariadb" POBSYNC_DB_ENGINE: "mariadb"
@@ -61,18 +80,24 @@ services:
db: db:
condition: service_healthy condition: service_healthy
ports: ports:
- "8010:8000" - "${POBSYNC_WEB_BIND:-0.0.0.0}:8010:8000"
volumes: volumes:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 30s
timeout: 10s
retries: 3
scheduler-mariadb: scheduler-mariadb:
profiles: ["mariadb"] profiles: ["mariadb"]
build: . build: .
command: python manage.py run_pobsync_scheduler --loop --interval 60 command: python manage.py run_pobsync_scheduler --loop --interval 60
restart: unless-stopped
environment: environment:
POBSYNC_DJANGO_DEBUG: "1" POBSYNC_DJANGO_DEBUG: "${POBSYNC_DJANGO_DEBUG:-0}"
POBSYNC_DJANGO_SECRET_KEY: "dev-only-change-me" POBSYNC_DJANGO_SECRET_KEY: "${POBSYNC_DJANGO_SECRET_KEY:-dev-only-change-me}"
POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}" POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}"
POBSYNC_HOME: "/opt/pobsync" POBSYNC_HOME: "/opt/pobsync"
POBSYNC_DB_ENGINE: "mariadb" POBSYNC_DB_ENGINE: "mariadb"
@@ -86,14 +111,20 @@ services:
volumes: volumes:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 30s
timeout: 10s
retries: 3
worker-mariadb: worker-mariadb:
profiles: ["mariadb"] profiles: ["mariadb"]
build: . build: .
command: python manage.py run_pobsync_worker --loop --interval 15 command: python manage.py run_pobsync_worker --loop --interval 15
restart: unless-stopped
environment: environment:
POBSYNC_DJANGO_DEBUG: "1" POBSYNC_DJANGO_DEBUG: "${POBSYNC_DJANGO_DEBUG:-0}"
POBSYNC_DJANGO_SECRET_KEY: "dev-only-change-me" POBSYNC_DJANGO_SECRET_KEY: "${POBSYNC_DJANGO_SECRET_KEY:-dev-only-change-me}"
POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}" POBSYNC_DJANGO_ALLOWED_HOSTS: "${POBSYNC_DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}"
POBSYNC_HOME: "/opt/pobsync" POBSYNC_HOME: "/opt/pobsync"
POBSYNC_DB_ENGINE: "mariadb" POBSYNC_DB_ENGINE: "mariadb"
@@ -107,10 +138,16 @@ services:
volumes: volumes:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 30s
timeout: 10s
retries: 3
db: db:
profiles: ["mariadb"] profiles: ["mariadb"]
image: mariadb:11 image: mariadb:11
restart: unless-stopped
environment: environment:
MARIADB_DATABASE: "pobsync" MARIADB_DATABASE: "pobsync"
MARIADB_USER: "pobsync" MARIADB_USER: "pobsync"

View File

@@ -9,6 +9,8 @@ description = "Pull-based rsync backup tool with hardlinked snapshots"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"Django>=5.2,<6.0", "Django>=5.2,<6.0",
"gunicorn>=23.0,<24.0",
"whitenoise>=6.9,<7.0",
"PyYAML>=6.0" "PyYAML>=6.0"
] ]

View File

@@ -4,5 +4,6 @@ set -eu
mkdir -p "$(dirname "${POBSYNC_SQLITE_PATH:-/var/lib/pobsync/pobsync.sqlite3}")" mkdir -p "$(dirname "${POBSYNC_SQLITE_PATH:-/var/lib/pobsync/pobsync.sqlite3}")"
python manage.py migrate --noinput python manage.py migrate --noinput
python manage.py collectstatic --noinput --clear
exec "$@" exec "$@"

View File

@@ -24,6 +24,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
@@ -85,6 +86,11 @@ USE_TZ = True
STATIC_URL = "static/" STATIC_URL = "static/"
STATIC_ROOT = os.getenv("POBSYNC_STATIC_ROOT", str(BASE_DIR / "var" / "static")) STATIC_ROOT = os.getenv("POBSYNC_STATIC_ROOT", str(BASE_DIR / "var" / "static"))
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"