Skip to content
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
35 changes: 28 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
# Copy to .env and adjust. Compose reads these automatically.
# Copy to .env and adjust. Shared by both deployment files:
# docker-compose.prod.yml — your own reverse proxy handles TLS (API on HTTP)
# docker-compose.tls.yml — this stack handles TLS, with automatic cert renewal

# Django
# ── Common (both) ────────────────────────────────────────────────────────────
DJANGO_SECRET_KEY=change-me
DJANGO_DEBUG=true
JWT_SECRET=change-me-too
DJANGO_DEBUG=false
# Released image tag to deploy (e.g. 0.1.0, latest).
API_TAG=latest

# Public hostname clients use. This is also the address entered in the mobile
# app as the API server URL, e.g. https://api.robozor.cz
DOMAIN=api.robozor.cz
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,api
DJANGO_CSRF_TRUSTED_ORIGINS=

# JWT (device access tokens)
JWT_SECRET=change-me-too
JWT_TTL_SECONDS=900
CHALLENGE_TTL_SECONDS=300

# PostgreSQL
POSTGRES_DB=meteorpointer
POSTGRES_USER=meteorpointer
POSTGRES_PASSWORD=meteorpointer
POSTGRES_PASSWORD=change-me-db

# ── Only when YOUR OWN proxy handles TLS (docker-compose.prod.yml) ───────────
# Optional: restrict the API to this machine so only a proxy running here can
# reach it (default exposes port 8000 on all interfaces).
# API_BIND=127.0.0.1:8000

# ── Only when THIS STACK handles TLS (docker-compose.tls.yml) ────────────────
# Your e-mail address turns on the real, auto-renewed certificate for DOMAIN.
# Leave it empty only for a quick local check with a self-signed certificate.
ACME_EMAIL=you@robozor.cz
# Bind the proxy to a specific host IP if port 443 is already used by something
# else on this machine, e.g.:
# PROXY_HTTP=192.168.0.49:80
# PROXY_HTTPS=192.168.0.49:443
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,32 @@ docker compose up -d
- Health check: `http://localhost:8000/healthz`
- Interaktivní dokumentace (OpenAPI): `http://localhost:8000/api/docs`

## Nasazení — dva scénáře (jeden `.env`)

Oba čtou stejný `.env`; každý compose soubor má v hlavičce popis i postup. Proměnné
specifické pro daný scénář jsou v `.env` v komentovaných sekcích.

### A) Vlastní reverzní proxy řeší TLS — `docker-compose.prod.yml`
Máš vlastní proxy (NAS / čelní server), která drží HTTPS certifikát a přeposílá na
tenhle stack. Stack běží na HTTP.
```bash
docker compose -f docker-compose.prod.yml up -d
```
V proxy nasměruj `https://api.robozor.cz` → tento stroj, port 8000.
**Adresa do mobilní aplikace:** `https://api.robozor.cz`

### B) TLS řeší tenhle stack, s automatickou obnovou — `docker-compose.tls.yml`
Nemáš vlastní proxy a chceš, aby si stack certifikát **sám pořídil a obnovoval**.
Potřebuje veřejnou doménu mířící na tento server a dostupné porty 80 a 443.
```bash
# v .env: DOMAIN=api.robozor.cz a ACME_EMAIL=tvuj@email
docker compose -f docker-compose.tls.yml up -d
```
**Adresa do mobilní aplikace:** `https://api.robozor.cz`

> Rychlá lokální kontrola bez domény: `DOMAIN=localhost` a prázdné `ACME_EMAIL`
> dají self-signed certifikát na `https://localhost` (prohlížeč varuje — očekávané).

## Testy

```bash
Expand Down
18 changes: 18 additions & 0 deletions backend/meteorpointer/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,25 @@ def env_list(name: str, default: str = "") -> list[str]:

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "insecure-dev-secret-change-me")
DEBUG = env_bool("DJANGO_DEBUG", True)

# Public domain — set in the stack env when deployed behind the HTTPS proxy.
# It drives ALLOWED_HOSTS and CSRF automatically, so it's configured once.
# Empty (the default) = plain HTTP mode (e.g. local LAN testing).
DOMAIN = os.environ.get("DOMAIN", "").strip()

ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,api")
CSRF_TRUSTED_ORIGINS = env_list("DJANGO_CSRF_TRUSTED_ORIGINS", "")
if DOMAIN:
https_origin = f"https://{DOMAIN}"
if https_origin not in CSRF_TRUSTED_ORIGINS:
CSRF_TRUSTED_ORIGINS.append(https_origin)
# localhost is already in the default ALLOWED_HOSTS.
if DOMAIN != "localhost" and DOMAIN not in ALLOWED_HOSTS:
ALLOWED_HOSTS.append(DOMAIN)

# Behind a TLS-terminating reverse proxy (Caddy): trust its X-Forwarded-Proto
# so Django treats forwarded requests as secure and builds https:// URLs.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

# --- JWT (device access tokens) ---
JWT_SECRET = os.environ.get("JWT_SECRET", SECRET_KEY)
Expand Down
38 changes: 31 additions & 7 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
# MeteorPointer — Docker Compose (production / released images)
# Pulls the published API image from GHCR instead of building locally.
# ╔══════════════════════════════════════════════════════════════════════════╗
# ║ DEPLOYMENT — TLS handled by YOUR OWN reverse proxy (stack serves HTTP) ║
# ╚══════════════════════════════════════════════════════════════════════════╝
#
# API_TAG=v0.1.0 docker compose -f docker-compose.prod.yml pull
# API_TAG=v0.1.0 docker compose -f docker-compose.prod.yml up -d
# Use this when you already run your own reverse proxy that holds the HTTPS
# certificate (e.g. on a NAS or a front server) and forwards traffic to this
# stack. The stack itself stays on plain HTTP behind that proxy.
#
# TLS / reverse proxy are handled by external infrastructure (not this stack).
# Containers: MeteorPointer-DB + MeteorPointer-API (HTTP on port 8000).
# Django trusts the proxy's X-Forwarded-Proto header, so it still knows the
# original request was HTTPS.
#
# Steps:
# 1) cp .env.example .env and set the "Common" + "your own proxy" values, e.g.
# DOMAIN=api.robozor.cz # the public hostname your proxy serves
# 2) docker compose -f docker-compose.prod.yml up -d
# 3) In your proxy, forward https://api.robozor.cz -> this machine, port 8000.
# (Set API_BIND=127.0.0.1:8000 if the proxy runs on this same machine and the
# API should not be reachable from the rest of the network.)
#
# Address to enter in the mobile app (API server URL): https://api.robozor.cz
#
# Logs: docker compose -f docker-compose.prod.yml logs -f api
# Stop: docker compose -f docker-compose.prod.yml down
#
# If you do NOT have your own proxy and want this stack to get and renew the
# certificate itself, use docker-compose.tls.yml instead.

name: meteorpointer

x-api-env: &api-env
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:?set in .env}
DJANGO_DEBUG: ${DJANGO_DEBUG:-false}
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:?set in .env}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,api}
DJANGO_CSRF_TRUSTED_ORIGINS: ${DJANGO_CSRF_TRUSTED_ORIGINS:-}
DOMAIN: ${DOMAIN:-}
JWT_SECRET: ${JWT_SECRET:?set in .env}
POSTGRES_DB: ${POSTGRES_DB:-meteorpointer}
POSTGRES_USER: ${POSTGRES_USER:-meteorpointer}
Expand Down Expand Up @@ -45,7 +67,9 @@ services:
db:
condition: service_healthy
ports:
- "8000:8000"
# Default 0.0.0.0:8000. Set API_BIND=127.0.0.1:8000 in .env so only a
# host proxy on this machine can reach the API.
- "${API_BIND:-8000}:8000"
restart: unless-stopped

volumes:
Expand Down
99 changes: 99 additions & 0 deletions docker-compose.tls.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# ╔══════════════════════════════════════════════════════════════════════════╗
# ║ DEPLOYMENT — TLS handled by THIS STACK, with automatic certificate renewal ║
# ╚══════════════════════════════════════════════════════════════════════════╝
#
# Use this when this server is reachable from the internet under your domain and
# you want the stack to manage the HTTPS certificate itself (you do NOT run your
# own reverse proxy). The built-in Caddy proxy obtains the certificate and renews
# it automatically before it expires.
#
# Containers: MeteorPointer-DB + MeteorPointer-API (internal) +
# MeteorPointer-Proxy (Caddy, the only one exposed, on ports 80 + 443).
#
# Requirements:
# - A public domain pointing to this server (e.g. api.robozor.cz).
# - Ports 80 and 443 reachable from the internet.
#
# Steps:
# 1) cp .env.example .env and set the "Common" + "this stack handles TLS" values:
# DOMAIN=api.robozor.cz
# ACME_EMAIL=you@robozor.cz # your e-mail — turns on the real certificate
# 2) docker compose -f docker-compose.tls.yml up -d
#
# Address to enter in the mobile app (API server URL): https://api.robozor.cz
#
# Logs: docker compose -f docker-compose.tls.yml logs -f proxy
# Stop: docker compose -f docker-compose.tls.yml down
#
# (Quick local check without a domain: DOMAIN=localhost and an empty ACME_EMAIL
# give a self-signed certificate at https://localhost — browsers warn, expected.)

name: meteorpointer

x-api-env: &api-env
DJANGO_DEBUG: ${DJANGO_DEBUG:-false}
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:?set in .env}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,api}
DJANGO_CSRF_TRUSTED_ORIGINS: ${DJANGO_CSRF_TRUSTED_ORIGINS:-}
DOMAIN: ${DOMAIN:?set DOMAIN in .env (e.g. api.robozor.cz or localhost)}
ACME_EMAIL: ${ACME_EMAIL:-}
JWT_SECRET: ${JWT_SECRET:?set in .env}
POSTGRES_DB: ${POSTGRES_DB:-meteorpointer}
POSTGRES_USER: ${POSTGRES_USER:-meteorpointer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env}
POSTGRES_HOST: db
POSTGRES_PORT: 5432

services:
db:
image: postgres:16-alpine
container_name: MeteorPointer-DB
environment:
POSTGRES_DB: ${POSTGRES_DB:-meteorpointer}
POSTGRES_USER: ${POSTGRES_USER:-meteorpointer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env}
volumes:
- meteorpointer_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-meteorpointer} -d ${POSTGRES_DB:-meteorpointer}"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped

api:
image: ghcr.io/bolidozor/meteorpointer-api:${API_TAG:-latest}
container_name: MeteorPointer-API
environment:
<<: *api-env
depends_on:
db:
condition: service_healthy
expose:
- "8000" # internal only; reached by the proxy over the docker network
restart: unless-stopped

proxy:
image: caddy:2-alpine
container_name: MeteorPointer-Proxy
depends_on:
- api
environment:
DOMAIN: ${DOMAIN:?set DOMAIN in .env}
# empty ACME_EMAIL -> internal self-signed CA; an email -> Let's Encrypt.
TLS_ARG: ${ACME_EMAIL:-internal}
ports:
# Override PROXY_HTTP/PROXY_HTTPS (e.g. 192.168.0.49:443) to bind a
# specific host IP when :443 is already taken by another service.
- "${PROXY_HTTP:-80}:80"
- "${PROXY_HTTPS:-443}:443"
volumes:
- ./docker/proxy/Caddyfile:/etc/caddy/Caddyfile:ro
- meteorpointer_caddy_data:/data
- meteorpointer_caddy_config:/config
restart: unless-stopped

volumes:
meteorpointer_db_data:
meteorpointer_caddy_data:
meteorpointer_caddy_config:
29 changes: 29 additions & 0 deletions docker/proxy/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# MeteorPointer reverse proxy — hostname-routed HTTPS.
#
# Routes by Host header, so this proxy can share a machine with other stacks as
# long as it binds a host IP/port that isn't already taken (see PROXY_HTTP /
# PROXY_HTTPS in docker-compose.tls.yml).
#
# {$DOMAIN} — the hostname to serve, e.g. api.bolidozor.cz (or localhost).
# {$TLS_ARG} — "internal" for a local self-signed cert (no public reachability
# needed), or an ACME e-mail (you@example.org) to get auto-renewed
# Let's Encrypt certs. Driven by ACME_EMAIL (empty => internal).
#
# Certs/account live in the caddy_data volume, so renewals survive restarts.

{$DOMAIN} {
encode gzip
tls {$TLS_ARG}

# --- API ---
@api path /v1/* /healthz /api/*
handle @api {
reverse_proxy api:8000
}

# --- Web frontend (added later) ---
# When MeteorPointer-FE exists: handle { reverse_proxy fe:80 }
handle {
reverse_proxy api:8000
}
}
Loading