Deployment
Docs¶
Docs is deployed via GitHub Pages HERE you can see the workflow
Docker Files¶
Backend¶
Dockerfile Backend
FROM python:3.14-slim # (1)!
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# (2)!
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# (3)!
RUN apt-get update && apt-get install -y --no-install-recommends \
libjpeg-dev \
zlib1g-dev \
gcc \
libc6-dev \
&& rm -rf /var/lib/apt/lists/*
# (4)!
COPY pyproject.toml uv.lock ./
# (5)!
RUN uv sync --frozen --no-dev
# (6)!
COPY . .
# (7)!
RUN mkdir -p /app/staticfiles /app/media && \
adduser --disabled-password --gecos "" django_user && \
chown -R django_user:django_user /app
# (8)!
USER django_user
# (9)!
EXPOSE 7996
RUN uv run python manage.py collectstatic --noinput
# (10)!
CMD ["sh", "-c", "uv run python manage.py migrate && \\
uv run uvicorn main.asgi:application --host 0.0.0.0 --port 7996 --workers 4"]
# (11)!
- Creates an image with python 3.14 slim
- Downloads uv
- Set the base workdir as app
- Downloads dependencies
- Copy pyproject.toml and uv.lock
- Downloads dependencies,
--frozenforces to useuv.lock,--no-devexcludes all dev dependencies - Copy all files
- Create media and static directories, also create a user and own the base directory
- Selected the created before user
- Create static files
- Running migrations and application with 4 workers
Frontend¶
Dockerfile Frontend
FROM node:22-alpine AS build-stage
# (1)!
WORKDIR /app
# (2)!
COPY package*.json ./
# (3)!
RUN npm ci
# (4)!
COPY . .
# (5)!
RUN npm run build
# (6)!
FROM alpine:latest # (7)!
RUN apk add --no-cache coreutils
# (8)!
WORKDIR /app
# (9)!
COPY --from=build-stage /app/dist /app/
# (10)!
CMD ["sh", "-c", "echo 'Sync complete' && tail -f /dev/null"]
# (11)!
- Creates an build image with node 22 alpine
- Set the base workdir as app
- Copy package.json and package-lock.json
- Run
npm run ci, is the same asnpm ibut usingpackage-lock.jsoninstead ofpackage.json - Copy all files
- And
run npm buildto create static files - Creates an production image with alpine
- Add coreutils dependency
- Set the base workdir as app
- From Previus build stage, copy the
app/distdir and added it toapp - Shows a message of
Sync completeand keep the container alive
Warning
This docker file, indeed does nothing more than copy the files of the build in a dir, serving it to other nginx, instead of creating an nginx inside of the container and proxy passing from the main nginx.
Docker Composes¶
Movies X Movies¶
docker-compose.yml
services:
db: # (1)!
image: postgres:15-alpine
container_name: movies_db
restart: always
env_file:
- .env # (2)!
environment:
- POSTGRES_DB=${DB_NAME}
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data # (3)!
healthcheck: # (4)!
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 5s
timeout: 5s
retries: 5
api: # (5)!
image: moviesxmovies/backend:latest # (6)!
container_name: movies_api
restart: always # (7)!
pull_policy: always # (8)!
env_file:
- .env # (9)!
ports:
- "7996:7996" # (10)!
depends_on:
db: # (11)!
condition: service_healthy
volumes: # (12)!
- static_data:/app/staticfiles
- media_data:/app/media
redis: # (21)!
image: redis:7-alpine # (22)!
container_name: movies_redis
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"] # (23)!
interval: 5s
timeout: 3s
retries: 5
worker: # (24)!
image: moviesxmovies/backend:latest # (25)!
restart: always
pull_policy: always
command: python manage.py rqworker default # (26)!
env_file:
- .env
depends_on: # (27)!
db:
condition: service_healthy
redis:
condition: service_healthy
volumes: # (28)!
- static_data:/app/staticfiles
- media_data:/app/media
web: # (13)!
image: moviesxmovies/frontend:latest
container_name: movies_frontend
restart: always # (14)!
pull_policy: always # (15)!
volumes: # (16)!
- frontend_data:/app
volumes:
postgres_data: # (17)!
static_data: # (18)!
driver: local
driver_opts:
type: none
o: bind
device: /var/www/moviesxmovies/static
media_data: # (19)!
driver: local
driver_opts:
type: none
o: bind
device: /var/www/moviesxmovies/media
frontend_data: # (20)!
driver: local
driver_opts:
type: none
o: bind
device: /var/www/moviesxmovies/app
- Creates a container of postgres as DB
- Adds
.envas enviroments variables - Links DB files with a volume named
postgres_data - Checks if DB is up or not
- Creates a container using our backend image of the API
- Our backend image in the latest tag
- If the launch fails, it retries
- If the docker image has changed, it pull it, to keep it updated
- Adds
.envas enviroments variables - Open port
7996to port7996of the container - Before starting up the container, it checks if the
dbcontainer is up - Links two volumnes, one used for static files, and the other one to media files
- Creates a container using our frontend image
- If the launch fails, it retries
- If the docker image has changed, it pull it, to keep it updated
- Links Frontend files with a volumne named
frontend_data - Creates a volumen for DB files
- Creates a volume for static files, and mount it on a directory of the machine
- Creates a volume for media files, and mount it on a directory of the machine
- Creates a volume for frontend files, and mount it on a directory of the machine
- Creates a container using an alpine redis image
- Official alpine redis image
- Creates a healthcheck to see if the container is ready
- Create a worker, which later we can run more container to be scalable
- Using the backend image, so it can get latest code
- And we override the base CMD command and instead we run a rqworker
- Before creating the worker, we have to secure that the db and redis are up
- And setup the volumes to be able to access static and media data
SonarQube¶
docker-compose.yml
version: "3"
services:
sonarqube:
image: mc1arke/sonarqube-with-community-branch-plugin:latest
env_file:
- .env
depends_on:
- sonar_db
environment:
SONAR_JDBC_URL=${POSTSQL_URL}
SONAR_JDBC_USERNAME=${POSTSQL_USERNAME}
SONAR_JDBC_PASSWORD=${POSTSQL_PASSWORD}
ports:
- "26453:9000"
volumes:
- sonarqube_conf:/opt/sonarqube/conf
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
- sonarqube_temp:/opt/sonarqube/temp
sonar_db:
image: postgres:13
env_file:
- .env
environment:
POSTGRES_USER=${POSTSQL_USERNAME}
POSTGRES_PASSWORD=${POSTSQL_PASSWORD}
POSTGRES_DB=${POSTSQL_DB}
volumes:
- sonar_db:/var/lib/postgresql
- sonar_db_data:/var/lib/postgresql/data
volumes:
sonarqube_conf:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
sonarqube_temp:
sonar_db:
sonar_db_data:
Nginx Server¶
moviesxmovies.conf
# Redirección y Seguridad HTTP
server {
listen 80;
server_name moviesxmovies.jonaykb.com;
if ($host !~* ^moviesxmovies\.jonaykb\.com$ ) { return 444; }
return 301 https://$host$request_uri;
}
# Servidor VPN — solo accesible desde 10.0.0.1 (WireGuard)
server {
listen 10.0.0.1:443 ssl http2;
server_name moviesxmovies.jonaykb.com;
ssl_certificate /etc/letsencrypt/live/moviesxmovies.jonaykb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/moviesxmovies.jonaykb.com/privkey.pem;
ssl_session_timeout 1d;
ssl_protocols TLSv1.2 TLSv1.3;
location /api/ {
proxy_pass http://127.0.0.1:7996/api/;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /docs {
proxy_pass https://moviesxmovies.github.io/MoviesXMovies;
}
# Grafana en la raíz para usuarios VPN
location / {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
location /admin/ {
proxy_pass http://127.0.0.1:7996/admin/;
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 /django-rq/ {
proxy_pass http://127.0.0.1:7996/django-rq/;
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/ {
alias /var/www/moviesxmovies/static/;
expires 30d;
add_header Cache-Control "public";
try_files $uri =404;
}
}
# Servidor Principal — acceso público
server {
listen 443 ssl http2;
server_name moviesxmovies.jonaykb.com;
ssl_certificate /etc/letsencrypt/live/moviesxmovies.jonaykb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/moviesxmovies.jonaykb.com/privkey.pem;
ssl_session_timeout 1d;
ssl_protocols TLSv1.2 TLSv1.3;
location /api/docs/ {
deny all;
}
location /api/schema/ {
deny all;
}
location /api/ {
proxy_pass http://127.0.0.1:7996/api/;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /admin/ {
deny all;
}
location /django-rq/ {
deny all;
}
location /media/ {
alias /var/www/moviesxmovies/media/;
try_files $uri =404;
}
location /docs {
proxy_pass https://moviesxmovies.github.io/MoviesXMovies;
}
location / {
root /var/www/moviesxmovies/app;
index index.html;
try_files $uri $uri/ /index.html;
}
}
Domain¶
The domain moviesxmovies.jonaykb.com is bought at Cloudflare with a record pointing to a Dynamic IP, but thanks to this project we keep our record updated and pointing to the correct IP