feat: add initial Docker Compose infrastructure

This commit is contained in:
2026-04-28 11:08:06 +02:00
parent 18ab0a8b99
commit 04f31c5105
15 changed files with 279 additions and 14 deletions

View File

@@ -77,7 +77,7 @@ All tests MUST be executed inside Docker containers.
Configure the canonical test command for this repository: Configure the canonical test command for this repository:
```bash ```bash
CHANGE_ME docker compose --env-file .env.example -f infra/docker/compose.yml config
``` ```
Examples: Examples:

24
.env.example Normal file
View File

@@ -0,0 +1,24 @@
# AzioneLab Docker Compose example environment.
# Copy this file to .env and replace placeholder values before deployment.
COMPOSE_PROJECT_NAME=azionelab
NGINX_HTTP_PORT=8080
BACKEND_HOST=backend
BACKEND_PORT=8000
FRONTEND_HOST=frontend
FRONTEND_PORT=8080
DJANGO_SECRET_KEY=change-me
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8080
DJANGO_DEBUG=false
POSTGRES_DB=azionelab
POSTGRES_USER=azionelab
POSTGRES_PASSWORD=change-me
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
DATABASE_URL=postgres://azionelab:change-me@postgres:5432/azionelab

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.env
.env.*
!.env.example

View File

@@ -0,0 +1,27 @@
# ADR-0007: Use Docker Compose for Deployment
Date: 2026-04-28
## Status
Accepted
## Context
AzioneLab needs a simple production-oriented deployment for a small theatre company website. The initial runtime services are Django with gunicorn, an Angular frontend served by nginx, PostgreSQL, and an nginx reverse proxy.
The project does not need Celery, Redis, a container orchestrator, or a more complex platform at this stage.
## Decision
Use Docker Compose as the initial deployment mechanism.
The Compose setup will define explicit `backend`, `frontend`, `postgres`, and `nginx` services under `infra/docker/compose.yml`. Configuration is provided through `.env`, PostgreSQL data is stored in a named volume, and only the reverse proxy publishes a host port.
## Consequences
- The deployment remains easy to understand, run, and review.
- The same topology can support local infrastructure checks and small production deployments.
- PostgreSQL persistence is explicit through a named volume.
- The setup can be replaced later if hosting or scaling needs outgrow Docker Compose.
- Operators must manage `.env`, backups, TLS, and image updates carefully.

View File

@@ -137,10 +137,12 @@ The initial deployment uses Docker Compose with these services:
- `nginx`: public reverse proxy and static frontend server; - `nginx`: public reverse proxy and static frontend server;
- `frontend`: Angular build stage or static asset build source; - `frontend`: Angular build stage or static asset build source;
- `backend`: Django application served by gunicorn; - `backend`: Django application served by gunicorn;
- `db`: PostgreSQL database. - `postgres`: PostgreSQL database.
Only nginx should be publicly exposed. The backend and database should be reachable only on the internal Compose network. Only nginx should be publicly exposed. The backend and database should be reachable only on the internal Compose network.
The initial Compose files live under `infra/docker/`. The backend and frontend images are placeholders until the Django and Angular applications are implemented.
## Architectural Constraints ## Architectural Constraints
- Keep the booking workflow synchronous and explicit. - Keep the booking workflow synchronous and explicit.
@@ -153,4 +155,8 @@ Only nginx should be publicly exposed. The backend and database should be reacha
## Relevant ADRs ## Relevant ADRs
No ADRs are recorded yet. The technology stack and initial constraints are documented here from the project request. - [ADR-0001: Use Django Monolith](adr/0001-use-django-monolith.md)
- [ADR-0002: Do Not Add an Async Task Queue Yet](adr/0002-no-async-task-queue.md)
- [ADR-0003: Use Opaque Tokens in QR Codes](adr/0003-qr-code-token-strategy.md)
- [ADR-0004: Use Email Confirmation for Reservations](adr/0004-email-confirmation-flow.md)
- [ADR-0007: Use Docker Compose for Deployment](adr/0007-use-docker-compose-for-deployment.md)

View File

@@ -5,10 +5,12 @@ AzioneLab should deploy with a simple Docker Compose topology:
- `nginx`: public reverse proxy and static frontend server; - `nginx`: public reverse proxy and static frontend server;
- `frontend`: Angular build source or build stage for static assets; - `frontend`: Angular build source or build stage for static assets;
- `backend`: Django 5.2 LTS application served by gunicorn; - `backend`: Django 5.2 LTS application served by gunicorn;
- `db`: PostgreSQL database. - `postgres`: PostgreSQL database.
Only nginx should expose public ports. The backend and database should stay on the internal Compose network. Only nginx should expose public ports. The backend and database should stay on the internal Compose network.
The initial Compose setup is located at `infra/docker/compose.yml`.
## Services ## Services
### nginx ### nginx
@@ -37,6 +39,8 @@ Deployment options:
The first option is preferred for a simple production deployment because nginx can serve immutable built assets without a long-running Node process. The first option is preferred for a simple production deployment because nginx can serve immutable built assets without a long-running Node process.
At the infrastructure placeholder stage, the `frontend` service serves a static placeholder page with nginx. The Angular build will replace this placeholder later.
### backend ### backend
The backend is a Django application served by gunicorn. The backend is a Django application served by gunicorn.
@@ -51,7 +55,9 @@ Responsibilities:
The backend should run database migrations before or during deployment through an explicit operational command, not as hidden startup magic unless that choice is documented later. The backend should run database migrations before or during deployment through an explicit operational command, not as hidden startup magic unless that choice is documented later.
### db At the infrastructure placeholder stage, the `backend` service runs gunicorn against a minimal placeholder WSGI application. The real Django application will replace it later.
### postgres
PostgreSQL is the only database service. PostgreSQL is the only database service.
@@ -84,6 +90,8 @@ Generated QR codes may also be generated on demand instead of stored as files. I
## Configuration ## Configuration
Copy `.env.example` to `.env` and replace all placeholder values before running or deploying the stack.
Required backend configuration: Required backend configuration:
- `DJANGO_SECRET_KEY`; - `DJANGO_SECRET_KEY`;
@@ -129,21 +137,25 @@ The exact commands will be finalized when application code and Compose files are
Expected production-style flow: Expected production-style flow:
```bash ```bash
docker compose build docker compose --env-file .env -f infra/docker/compose.yml build
docker compose run --rm backend python manage.py migrate docker compose --env-file .env -f infra/docker/compose.yml run --rm backend python manage.py migrate
docker compose run --rm backend python manage.py collectstatic --noinput docker compose --env-file .env -f infra/docker/compose.yml run --rm backend python manage.py collectstatic --noinput
docker compose up -d docker compose --env-file .env -f infra/docker/compose.yml up -d
``` ```
Expected validation commands: Expected validation commands:
```bash ```bash
docker compose config docker compose --env-file .env.example -f infra/docker/compose.yml config
docker compose run --rm backend python manage.py check --deploy docker compose --env-file .env -f infra/docker/compose.yml run --rm backend python manage.py check --deploy
docker compose run --rm backend python manage.py test docker compose --env-file .env -f infra/docker/compose.yml run --rm backend python manage.py test
``` ```
The repository does not yet define the canonical Docker-based test command. The canonical repository check for the current infrastructure stage is:
```bash
docker compose --env-file .env.example -f infra/docker/compose.yml config
```
## Rollback ## Rollback

View File

@@ -115,12 +115,15 @@ Expected secret configuration:
Use environment variables, Docker secrets, or deployment-managed secret injection. Documentation and example configuration should use placeholders only. Use environment variables, Docker secrets, or deployment-managed secret injection. Documentation and example configuration should use placeholders only.
For the Docker Compose setup, copy `.env.example` to `.env` and replace placeholder values outside version control. The repository ignores `.env` and `.env.*` files except `.env.example`.
## Deployment Security ## Deployment Security
Deployment should follow least privilege: Deployment should follow least privilege:
- expose only nginx publicly; - expose only nginx publicly;
- keep backend and database on an internal Docker network; - keep backend and database on an internal Docker network;
- do not publish backend, frontend, or PostgreSQL ports to the host in production;
- avoid privileged containers; - avoid privileged containers;
- use explicit image tags rather than `latest`; - use explicit image tags rather than `latest`;
- persist PostgreSQL data in a named volume; - persist PostgreSQL data in a named volume;

View File

@@ -7,7 +7,7 @@ All tests should run inside Docker containers.
## Canonical test command ## Canonical test command
```bash ```bash
CHANGE_ME docker compose --env-file .env.example -f infra/docker/compose.yml config
``` ```
## Test categories ## Test categories

View File

@@ -0,0 +1,22 @@
FROM python:3.13.4-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN pip install --no-cache-dir \
"Django==5.2.3" \
"djangorestframework==3.16.0" \
"gunicorn==23.0.0" \
"psycopg[binary]==3.2.9"
RUN useradd --create-home --shell /usr/sbin/nologin appuser
COPY placeholder_wsgi.py /app/placeholder_wsgi.py
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "placeholder_wsgi:application"]

View File

@@ -0,0 +1,10 @@
def application(environ, start_response):
body = b"AzioneLab backend placeholder\n"
start_response(
"503 Service Unavailable",
[
("Content-Type", "text/plain; charset=utf-8"),
("Content-Length", str(len(body))),
],
)
return [body]

77
infra/docker/compose.yml Normal file
View File

@@ -0,0 +1,77 @@
services:
backend:
build:
context: ./backend
image: azionelab-backend:local
environment:
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS}
DJANGO_CSRF_TRUSTED_ORIGINS: ${DJANGO_CSRF_TRUSTED_ORIGINS}
DJANGO_DEBUG: ${DJANGO_DEBUG:-false}
DATABASE_URL: ${DATABASE_URL}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
expose:
- "${BACKEND_PORT:-8000}"
depends_on:
postgres:
condition: service_healthy
networks:
- internal
restart: unless-stopped
frontend:
build:
context: ./frontend
image: azionelab-frontend:local
expose:
- "${FRONTEND_PORT:-8080}"
networks:
- internal
restart: unless-stopped
postgres:
image: postgres:16.3-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
interval: 10s
timeout: 5s
retries: 5
networks:
- internal
restart: unless-stopped
nginx:
image: nginx:1.27.0-alpine
ports:
- "${NGINX_HTTP_PORT:-8080}:80"
environment:
BACKEND_HOST: ${BACKEND_HOST:-backend}
BACKEND_PORT: ${BACKEND_PORT:-8000}
FRONTEND_HOST: ${FRONTEND_HOST:-frontend}
FRONTEND_PORT: ${FRONTEND_PORT:-8080}
NGINX_ENVSUBST_FILTER: "^(BACKEND_HOST|BACKEND_PORT|FRONTEND_HOST|FRONTEND_PORT)$"
volumes:
- ./nginx/templates:/etc/nginx/templates:ro
depends_on:
- backend
- frontend
networks:
- internal
restart: unless-stopped
volumes:
postgres_data:
networks:
internal:
driver: bridge

View File

@@ -0,0 +1,6 @@
FROM nginx:1.27.0-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY html/ /usr/share/nginx/html/
EXPOSE 8080

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AzioneLab</title>
</head>
<body>
<main>
<h1>AzioneLab frontend placeholder</h1>
<p>The Angular application build will replace this page.</p>
</main>
</body>
</html>

View File

@@ -0,0 +1,11 @@
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -0,0 +1,50 @@
upstream azionelab_backend {
server ${BACKEND_HOST}:${BACKEND_PORT};
}
upstream azionelab_frontend {
server ${FRONTEND_HOST}:${FRONTEND_PORT};
}
server {
listen 80;
server_name _;
client_max_body_size 10m;
location /api/ {
proxy_pass http://azionelab_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /admin/ {
proxy_pass http://azionelab_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ {
proxy_pass http://azionelab_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /media/ {
proxy_pass http://azionelab_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://azionelab_frontend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}