@@ -8,3 +8,5 @@ __pycache__
*.patch
venv
.venv
+.env.*
+docker/certs
new file mode 100644
@@ -0,0 +1,8 @@
+# Development only; not secret.
+SECRET_KEY=dev-only-not-secret
+DEBUG=1
+ALLOWED_HOSTS=localhost,127.0.0.1
+MYSQL_DATABASE=errorreport
+MYSQL_USER=errorreport
+MYSQL_PASSWORD=testingpw
+#CSP_REPORT_ONLY=1
new file mode 100644
@@ -0,0 +1,5 @@
+# Development only; not secret.
+MYSQL_DATABASE=errorreport
+MYSQL_USER=errorreport
+MYSQL_PASSWORD=testingpw
+MYSQL_ROOT_PASSWORD=testingpw
new file mode 100644
@@ -0,0 +1,9 @@
+# Set real values in a local commit on the server; never push them.
+# Commented-out secrets stop the service starting.
+#SECRET_KEY=<set this here>
+DEBUG=0
+ALLOWED_HOSTS=errors.yoctoproject.org
+MYSQL_DATABASE=errorreport
+MYSQL_USER=errorreport
+#MYSQL_PASSWORD=<set this here, same as in .env.prod.db>
+#CSP_REPORT_ONLY=1
new file mode 100644
@@ -0,0 +1,6 @@
+# Set real values in a local commit on the server; never push them.
+# Commented-out secrets stop the service starting.
+MYSQL_DATABASE=errorreport
+MYSQL_USER=errorreport
+#MYSQL_PASSWORD=<set this here, same as in .env.prod.app>
+#MYSQL_ROOT_PASSWORD=<set this here>
new file mode 100644
@@ -0,0 +1,9 @@
+# Set real values in a local commit on the server; never push them.
+# Commented-out secrets stop the service starting.
+#SECRET_KEY=<set this here>
+DEBUG=0
+#ALLOWED_HOSTS=<set this here>
+MYSQL_DATABASE=errorreport
+MYSQL_USER=errorreport
+#MYSQL_PASSWORD=<set this here, same as in .env.staging.db>
+#CSP_REPORT_ONLY=1
new file mode 100644
@@ -0,0 +1,6 @@
+# Set real values in a local commit on the server; never push them.
+# Commented-out secrets stop the service starting.
+MYSQL_DATABASE=errorreport
+MYSQL_USER=errorreport
+#MYSQL_PASSWORD=<set this here, same as in .env.staging.app>
+#MYSQL_ROOT_PASSWORD=<set this here>
@@ -8,3 +8,4 @@ pull-*/
venv
*.sqlite3
.env
+docker/certs/
@@ -41,6 +41,45 @@ In order to send an error report to the server, run the "send-error-report file_
For more information on error reporting and log collection see: http://www.yoctoproject.org/docs/current/dev-manual/dev-manual.html#using-the-error-reporting-tool
+Docker
+------
+
+docker-compose.yaml runs a development setup: MySQL, the application in
+gunicorn, and nginx serving it on http://localhost:8080. Its settings are in
+.env.dev.app and .env.dev.db, which hold development-only values.
+
+ docker compose up -d --build
+
+To run the tests in a container, as CI does:
+
+ docker/run-tests.sh
+
+docker-compose.staging.yaml and docker-compose.prod.yaml serve the site over
+HTTPS on ports 80 and 443, with a certificate from Let's Encrypt. Staging uses
+Let's Encrypt's staging server, whose certificates browsers don't trust but
+whose rate limits are generous; production uses the real server. Taking prod
+as the example:
+
+1. Set the real values in .env.prod.app and .env.prod.db (SECRET_KEY,
+ ALLOWED_HOSTS and the database passwords) in a local commit on the server.
+ Never push them.
+2. Get the first certificate. nginx needs it to start, so certbot answers the
+ challenge itself on port 80 (for staging, use docker-compose.staging.yaml
+ and add --test-cert):
+
+ docker compose -f docker-compose.prod.yaml run --rm -p 80:80 \
+ --entrypoint certbot certbot certonly --standalone \
+ --cert-name error-report-web -d errors.example.org \
+ -m you@example.org --agree-tos --no-eff-email
+
+ The certificate is stored in docker/certs, which git ignores.
+3. Start everything:
+
+ docker compose -f docker-compose.prod.yaml up -d --build
+
+The certbot container renews the certificate from the server that issued it,
+and nginx reloads every 6 hours to pick it up.
+
Content Security Policy
-----------------------
new file mode 100644
@@ -0,0 +1,55 @@
+# Prod: nginx with Let's Encrypt TLS in front of gunicorn. See README.
+services:
+ db:
+ image: mysql:8.4
+ restart: unless-stopped
+ volumes:
+ - data:/var/lib/mysql
+ env_file:
+ - .env.prod.db
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
+ timeout: 20s
+ retries: 10
+
+ backend:
+ build: .
+ command: sh -c "python3 manage.py migrate --noinput && python3 manage.py collectstatic --noinput && gunicorn project.wsgi --bind=:8000 --workers=4 --timeout=60"
+ restart: unless-stopped
+ volumes:
+ - static:/app/staticfiles
+ env_file:
+ - .env.prod.app
+ depends_on:
+ db:
+ condition: service_healthy
+
+ web:
+ image: nginx:1.30
+ restart: unless-stopped
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./docker/nginx-ssl.conf:/etc/nginx/nginx.conf:ro
+ - static:/usr/share/nginx/html/static:ro
+ - ./docker/certs:/etc/letsencrypt:ro
+ - certbot-www:/var/www/certbot:ro
+ # Reload every 6 hours to pick up renewed certificates.
+ command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
+ depends_on:
+ - backend
+
+ # Renews from whichever Let's Encrypt server issued the certificate.
+ certbot:
+ image: certbot/certbot
+ restart: unless-stopped
+ volumes:
+ - ./docker/certs:/etc/letsencrypt
+ - certbot-www:/var/www/certbot
+ entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'"
+
+volumes:
+ data:
+ static:
+ certbot-www:
new file mode 100644
@@ -0,0 +1,55 @@
+# Staging: nginx with Let's Encrypt TLS in front of gunicorn. See README.
+services:
+ db:
+ image: mysql:8.4
+ restart: unless-stopped
+ volumes:
+ - data:/var/lib/mysql
+ env_file:
+ - .env.staging.db
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
+ timeout: 20s
+ retries: 10
+
+ backend:
+ build: .
+ command: sh -c "python3 manage.py migrate --noinput && python3 manage.py collectstatic --noinput && gunicorn project.wsgi --bind=:8000 --workers=4 --timeout=60"
+ restart: unless-stopped
+ volumes:
+ - static:/app/staticfiles
+ env_file:
+ - .env.staging.app
+ depends_on:
+ db:
+ condition: service_healthy
+
+ web:
+ image: nginx:1.30
+ restart: unless-stopped
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./docker/nginx-ssl.conf:/etc/nginx/nginx.conf:ro
+ - static:/usr/share/nginx/html/static:ro
+ - ./docker/certs:/etc/letsencrypt:ro
+ - certbot-www:/var/www/certbot:ro
+ # Reload every 6 hours to pick up renewed certificates.
+ command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
+ depends_on:
+ - backend
+
+ # Renews from whichever Let's Encrypt server issued the certificate.
+ certbot:
+ image: certbot/certbot
+ restart: unless-stopped
+ volumes:
+ - ./docker/certs:/etc/letsencrypt
+ - certbot-www:/var/www/certbot
+ entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'"
+
+volumes:
+ data:
+ static:
+ certbot-www:
@@ -5,15 +5,13 @@ services:
restart: always
volumes:
- data:/var/lib/mysql
- environment:
- MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE: ${MYSQL_DATABASE}
- MYSQL_USER: ${MYSQL_USER}
- MYSQL_PASSWORD: ${MYSQL_PASSWORD}
+ env_file:
+ - .env.dev.db
ports:
- "3306:3306"
healthcheck:
- test: ["CMD", "mysql", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}", "-e", "SELECT 1"]
+ # TCP: MySQL's init-time server only listens on the socket.
+ test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
timeout: 20s
retries: 10
@@ -27,7 +25,7 @@ services:
volumes:
- static:/app/staticfiles
env_file:
- - .env
+ - .env.dev.app
depends_on:
db:
condition: service_healthy
new file mode 100644
@@ -0,0 +1,72 @@
+# nginx with TLS for docker-compose.{staging,prod}.yaml; see README.
+#
+# SPDX-License-Identifier: MIT
+
+error_log /dev/stdout info;
+worker_processes 1;
+pid /tmp/nginx.pid;
+
+events {
+ worker_connections 1024;
+ accept_mutex off;
+}
+
+http {
+ include mime.types;
+ default_type application/octet-stream;
+ access_log /dev/stdout combined;
+ sendfile on;
+
+ upstream app_server {
+ server backend:8000 fail_timeout=0;
+ }
+
+ # Reports can be up to DATA_UPLOAD_MAX_MEMORY_SIZE (10MB).
+ client_max_body_size 10m;
+ keepalive_timeout 20;
+
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ # Overwrites the client's value; Django trusts it.
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Host $http_host;
+ proxy_redirect off;
+
+ server {
+ listen 80 default_server;
+ server_name _;
+
+ location /.well-known/acme-challenge/ {
+ root /var/www/certbot;
+ }
+
+ # send-error-report may POST over HTTP; a redirect would drop it.
+ location /ClientPost/ {
+ proxy_pass http://app_server;
+ }
+
+ location / {
+ return 301 https://$host$request_uri;
+ }
+ }
+
+ server {
+ listen 443 ssl default_server;
+ server_name _;
+ ssl_certificate /etc/letsencrypt/live/error-report-web/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/error-report-web/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_session_cache shared:SSL:12m;
+ ssl_session_timeout 12m;
+
+ # path for static files
+ root /usr/share/nginx/html;
+
+ location / {
+ try_files $uri @proxy_to_app;
+ }
+
+ location @proxy_to_app {
+ proxy_pass http://app_server;
+ }
+ }
+}
Follow layerindex-web's timo/docker-compose-env branch: tracked .env.{dev,staging,prod}.{app,db}, with real values in a local commit on the server. Staging and prod leave secrets commented out, so nothing starts with a known password, and serve HTTPS with Let's Encrypt (staging from its staging server); certbot renews. /ClientPost/ stays on HTTP, as send-error-report may post there. The db healthcheck no longer needs the root password. Keep docker/certs out of git and the env files out of the image. AI-Generated: Claude Opus 5.5 Signed-off-by: Tim Orling <tim.orling@konsulko.com> --- .dockerignore | 2 ++ .env.dev.app | 8 +++++ .env.dev.db | 5 +++ .env.prod.app | 9 +++++ .env.prod.db | 6 ++++ .env.staging.app | 9 +++++ .env.staging.db | 6 ++++ .gitignore | 1 + README | 39 ++++++++++++++++++++ docker-compose.prod.yaml | 55 ++++++++++++++++++++++++++++ docker-compose.staging.yaml | 55 ++++++++++++++++++++++++++++ docker-compose.yaml | 12 +++---- docker/nginx-ssl.conf | 72 +++++++++++++++++++++++++++++++++++++ 13 files changed, 272 insertions(+), 7 deletions(-) create mode 100644 .env.dev.app create mode 100644 .env.dev.db create mode 100644 .env.prod.app create mode 100644 .env.prod.db create mode 100644 .env.staging.app create mode 100644 .env.staging.db create mode 100644 docker-compose.prod.yaml create mode 100644 docker-compose.staging.yaml create mode 100644 docker/nginx-ssl.conf