🧑‍💻(docker) split frontend to another file

This commit aims at improving the user experience:
- Use a dedicated `Dockerfile` for the frontend
- Run the backend and frontend in "watch"/dev mode in Docker
- Do not start all Docker instances for small tasks
This commit is contained in:
Quentin BEY
2025-06-12 15:10:37 +02:00
parent 4dfd682cb6
commit 213656fc2e
21 changed files with 257 additions and 132 deletions

View File

@@ -40,7 +40,7 @@ jobs:
name: Run trivy scan (frontend)
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '--target frontend-production -f Dockerfile'
docker-build-args: '--target frontend-production -f src/frontend/Dockerfile'
docker-image-name: 'docker.io/lasuite/people-frontend:${{ github.sha }}'
build-and-push-backend:
@@ -105,6 +105,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -167,6 +167,8 @@ jobs:
COMPOSE_DOCKER_CLI_BUILD: 1
run: |
docker compose build --pull --build-arg BUILDKIT_INLINE_CACHE=1
make update-keycloak-realm-app
make add-dev-rsa-private-key-to-env
make run
- name: Apply DRF migrations
@@ -177,10 +179,6 @@ jobs:
run: |
make demo FLUSH_ARGS='--no-input'
- name: Setup Dimail DB
run: |
make dimail-setup-db
- name: Run e2e tests
run: cd src/frontend/ && yarn e2e:test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

View File

@@ -12,6 +12,10 @@ and this project adheres to
- ✨(resource-server) add SCIM /Me endpoint #895
### Changed
- 🧑‍💻(docker) split frontend to another file #924
## [1.17.0] - 2025-06-11
### Added

View File

@@ -10,61 +10,6 @@ RUN python -m pip install --upgrade pip setuptools
RUN apk update && \
apk upgrade
### ---- Front-end dependencies image ----
FROM node:20 AS frontend-deps
WORKDIR /deps
COPY ./src/frontend/package.json ./package.json
COPY ./src/frontend/yarn.lock ./yarn.lock
COPY ./src/frontend/apps/desk/package.json ./apps/desk/package.json
COPY ./src/frontend/packages/i18n/package.json ./packages/i18n/package.json
COPY ./src/frontend/packages/eslint-config-people/package.json ./packages/eslint-config-people/package.json
RUN yarn --frozen-lockfile
### ---- Front-end builder dev image ----
FROM node:20 AS frontend-builder-dev
WORKDIR /builder
COPY --from=frontend-deps /deps/node_modules ./node_modules
COPY ./src/frontend .
WORKDIR ./apps/desk
### ---- Front-end builder image ----
FROM frontend-builder-dev AS frontend-builder
RUN yarn build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.27-alpine AS frontend-production
USER root
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2
USER nginx
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}
COPY --from=frontend-builder \
/builder/apps/desk/out \
/usr/share/nginx/html
COPY ./src/frontend/apps/desk/conf/default.conf /etc/nginx/conf.d
# Copy entrypoint
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
CMD ["nginx", "-g", "daemon off;"]
# ---- Back-end builder image ----
FROM base AS back-builder

View File

@@ -41,11 +41,9 @@ DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
COMPOSE_EXEC = $(COMPOSE) exec
COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-dev
COMPOSE_RUN = $(COMPOSE) run --rm
COMPOSE_RUN = $(COMPOSE) run --rm --no-deps
COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev
COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT) -timeout 60s
WAIT_KC_DB = $(COMPOSE_RUN) dockerize -wait tcp://kc_postgresql:5432 -timeout 60s
# -- Backend
MANAGE = $(COMPOSE_RUN_APP) python manage.py
@@ -78,19 +76,33 @@ create-env-files: \
env.d/development/kc_postgresql
.PHONY: create-env-files
add-dev-rsa-private-key-to-env: ## Add a generated RSA private key to the env file
@echo "Generating RSA private key PEM for development..."
@mkdir -p env.d/development/rsa
@openssl genrsa -out env.d/development/rsa/private.pem 2048
@echo -n "\nOAUTH2_PROVIDER_OIDC_RSA_PRIVATE_KEY=\"" >> env.d/development/common
@openssl rsa -in env.d/development/rsa/private.pem -outform PEM >> env.d/development/common
@echo "\"" >> env.d/development/common
@rm -rf env.d/development/rsa
.PHONY: add-dev-rsa-private-key-to-env
update-keycloak-realm-app: ## Create the Keycloak realm for the project
@echo "$(BOLD)Creating Keycloak realm for 'app'$(RESET)"
@sed -i 's|http://app-dev:8000|http://app:8000|g' ./docker/auth/realm.json
.PHONY: update-keycloak-realm-app
bootstrap: ## Prepare Docker images for the project and install frontend dependencies
bootstrap: \
data/media \
data/static \
create-env-files \
build \
run \
run-dev \
migrate \
back-i18n-compile \
mails-install \
mails-build \
dimail-setup-db \
install-front-desk
dimail-setup-db
.PHONY: bootstrap
# -- Docker/compose
@@ -106,19 +118,14 @@ logs: ## display app-dev logs (follow mode)
@$(COMPOSE) logs -f app-dev
.PHONY: logs
run: ## start the wsgi (production) and development server
@$(COMPOSE) up --force-recreate -d nginx
@$(COMPOSE) up --force-recreate -d app-dev
@$(COMPOSE) up --force-recreate -d celery-dev
@$(COMPOSE) up --force-recreate -d celery-beat-dev
@$(COMPOSE) up --force-recreate -d flower-dev
@$(COMPOSE) up --force-recreate -d keycloak
@$(COMPOSE) up -d dimail
@echo "Wait for postgresql to be up..."
@$(WAIT_KC_DB)
@$(WAIT_DB)
run: ## start the wsgi (production) and servers with production Docker images
@$(COMPOSE) up --force-recreate --detach app frontend celery celery-beat nginx maildev
.PHONY: run
run-dev: ## start the servers in development mode (watch) Docker images
@$(COMPOSE) up --force-recreate --detach app-dev frontend-dev celery-dev celery-beat-dev nginx maildev
.PHONY: run-dev
status: ## an alias for "docker compose ps"
@$(COMPOSE) ps
.PHONY: status
@@ -187,22 +194,16 @@ test-coverage: ## compute, display and save test coverage
makemigrations: ## run django makemigrations for the people project.
@echo "$(BOLD)Running makemigrations$(RESET)"
@$(COMPOSE) up -d postgresql
@$(WAIT_DB)
@$(MANAGE) makemigrations $(ARGS)
.PHONY: makemigrations
migrate: ## run django migrations for the people project.
@echo "$(BOLD)Running migrations$(RESET)"
@$(COMPOSE) up -d postgresql
@$(WAIT_DB)
@$(MANAGE) migrate $(ARGS)
.PHONY: migrate
showmigrations: ## run django showmigrations for the people project.
@echo "$(BOLD)Running showmigrations$(RESET)"
@$(COMPOSE) up -d postgresql
@$(WAIT_DB)
@$(MANAGE) showmigrations $(ARGS)
.PHONY: showmigrations

View File

@@ -33,7 +33,7 @@ The easiest way to start working on the project is to use GNU Make:
$ make bootstrap
```
This command builds the `app` container, installs dependencies, performs
This command builds the `app-dev` container, installs dependencies, performs
database migrations and compile translations. It's a good idea to use this
command each time you are pulling code from the project repository to avoid
dependency-related or migration-related issues.
@@ -46,6 +46,12 @@ Note that if you need to run them afterward, you can use the eponym Make rule:
$ make run
```
or if you want to run them in development mode (with live reloading):
```bash
$ make run-dev
```
You can check all available Make rules using:
```bash

View File

@@ -64,6 +64,23 @@ function _dc_run() {
_docker_compose run --rm $user_args "$@"
}
# _dc_run_no_deps: wrap docker compose run command without dependencies
#
# usage: _dc_run_no_deps [options] [ARGS...]
#
# options: docker compose run command options
# ARGS : docker compose run command arguments
function _dc_run_no_deps() {
_set_user
user_args="--user=$USER_ID"
if [ -z $USER_ID ]; then
user_args=""
fi
_docker_compose run --no-deps --rm $user_args "$@"
}
# _dc_exec: wrap docker compose exec command
#
# usage: _dc_exec [options] [ARGS...]

View File

@@ -35,4 +35,4 @@ fi
# Fix docker vs local path when project sources are mounted as a volume
read -ra paths <<< "$(echo "${paths[@]}" | sed "s|src/backend/||g")"
_dc_run app-dev pylint "${paths[@]}" "${args[@]}"
_dc_run_no_deps app-dev pylint "${paths[@]}" "${args[@]}"

View File

@@ -6,6 +6,11 @@ services:
- env.d/development/postgresql
ports:
- "15432:5432"
healthcheck:
test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ]
interval: 1s
timeout: 2s
retries: 300
redis:
image: redis:5
@@ -23,6 +28,7 @@ services:
DOCKER_USER: ${DOCKER_USER:-1000}
user: ${DOCKER_USER:-1000}
image: people:backend-development
pull_policy: never
environment:
- PYLINTHOME=/app/.pylint.d
- DJANGO_CONFIGURATION=Development
@@ -37,10 +43,68 @@ services:
- ./data/media:/data/media
- ./data/static:/data/static
depends_on:
- dimail
- postgresql
- maildev
- redis
postgresql:
condition: service_healthy
restart: true
dimail:
condition: service_started
maildev:
condition: service_started
redis:
condition: service_started
frontend-dev:
user: "${DOCKER_USER:-1000}"
build:
context: .
dockerfile: ./src/frontend/Dockerfile
target: frontend-dev
image: people:frontend-development
pull_policy: never
volumes:
- ./src/frontend:/home/frontend
- /home/frontend/node_modules
- /home/frontend/apps/desk/node_modules
ports:
- "3000:3000"
app:
build:
context: .
target: backend-production
args:
DOCKER_USER: ${DOCKER_USER:-1000}
user: ${DOCKER_USER:-1000}
image: people:backend-production
environment:
- DJANGO_CONFIGURATION=Development
env_file:
- env.d/development/common
- env.d/development/france
- env.d/development/postgresql
ports:
- "8071:8000"
volumes:
- ./data/media:/data/media
depends_on:
postgresql:
condition: service_healthy
restart: true
redis:
condition: service_started
frontend:
user: "${DOCKER_USER:-1000}"
build:
context: .
dockerfile: ./src/frontend/Dockerfile
target: frontend-production
args:
API_ORIGIN: "http://localhost:8071"
image: people:frontend-production
pull_policy: build
ports:
- "3000:3000"
celery-dev:
user: ${DOCKER_USER:-1000}
@@ -56,7 +120,11 @@ services:
- ./data/media:/data/media
- ./data/static:/data/static
depends_on:
- app-dev
postgresql:
condition: service_healthy
restart: true
app-dev:
condition: service_started
celery-beat-dev:
user: ${DOCKER_USER:-1000}
@@ -72,26 +140,9 @@ services:
- ./data/media:/data/media
- ./data/static:/data/static
depends_on:
- app-dev
app:
build:
context: .
target: backend-production
args:
DOCKER_USER: ${DOCKER_USER:-1000}
user: ${DOCKER_USER:-1000}
image: people:backend-production
environment:
- DJANGO_CONFIGURATION=Demo
env_file:
- env.d/development/common
- env.d/development/postgresql
volumes:
- ./data/media:/data/media
depends_on:
- postgresql
- redis
postgresql:
condition: service_healthy
restart: true
celery:
user: ${DOCKER_USER:-1000}
@@ -103,7 +154,29 @@ services:
- env.d/development/common
- env.d/development/postgresql
depends_on:
- app
postgresql:
condition: service_healthy
restart: true
app:
condition: service_started
celery-beat:
user: ${DOCKER_USER:-1000}
image: people:backend-production
command: ["celery", "-A", "people.celery_app", "beat", "-l", "INFO"]
environment:
- DJANGO_CONFIGURATION=Demo
env_file:
- env.d/development/common
- env.d/development/postgresql
volumes:
- ./src/backend:/app
- ./data/media:/data/media
- ./data/static:/data/static
depends_on:
postgresql:
condition: service_healthy
restart: true
nginx:
image: nginx:1.25
@@ -112,12 +185,9 @@ services:
volumes:
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
- app
- keycloak
dockerize:
image: jwilder/dockerize
platform: linux/x86_64
keycloak:
condition: service_healthy
restart: true
crowdin:
image: crowdin/cli:4.6.1
@@ -142,6 +212,11 @@ services:
- "5433:5432"
env_file:
- env.d/development/kc_postgresql
healthcheck:
test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ]
interval: 1s
timeout: 2s
retries: 300
keycloak:
image: quay.io/keycloak/keycloak:20.0.1
@@ -158,6 +233,8 @@ services:
- --hostname-admin-url=http://localhost:8083/
- --hostname-strict=false
- --hostname-strict-https=false
- --health-enabled=true
- --metrics-enabled=true
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
@@ -168,10 +245,17 @@ services:
KC_DB_USERNAME: people
KC_DB_SCHEMA: public
PROXY_ADDRESS_FORWARDING: 'true'
healthcheck:
test: [ "CMD", "curl", "--head", "-fsS", "http://localhost:8080/health/ready" ]
interval: 1s
timeout: 2s
retries: 300
ports:
- "8080:8080"
depends_on:
- kc_postgresql
kc_postgresql:
condition: service_healthy
restart: true
dimail:
entrypoint: /opt/dimail-api/start-dev.sh

View File

@@ -26,7 +26,7 @@
"oauth2DeviceCodeLifespan": 600,
"oauth2DevicePollingInterval": 5,
"enabled": true,
"sslRequired": "external",
"sslRequired": "none",
"registrationAllowed": true,
"registrationEmailAsUsername": false,
"rememberMe": true,

View File

@@ -3,6 +3,7 @@ DJANGO_ALLOWED_HOSTS=*
DJANGO_SECRET_KEY=ThisIsAnExampleKeyForDevPurposeOnly
DJANGO_SETTINGS_MODULE=people.settings
DJANGO_SUPERUSER_PASSWORD=admin
DJANGO_CORS_ALLOWED_ORIGINS=http://localhost:3000
# Python
PYTHONPATH=/app

View File

@@ -2,7 +2,9 @@
SUSTAINED_THROTTLE_RATES="200/hour"
BURST_THROTTLE_RATES="200/minute"
OAUTH2_PROVIDER_OIDC_ENABLED = True
OAUTH2_PROVIDER_VALIDATOR_CLASS: "mailbox_oauth2.validators.ProConnectValidator"
OIDC_ORGANIZATION_REGISTRATION_ID_FIELD="siret"
OAUTH2_PROVIDER_OIDC_ENABLED=True
OAUTH2_PROVIDER_VALIDATOR_CLASS="mailbox_oauth2.validators.ProConnectValidator"
INSTALLED_PLUGINS=plugins.la_suite

View File

@@ -34,6 +34,7 @@ dependencies = [
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",
"django-extensions==4.1",
"django-lasuite==0.0.9",
"django-oauth-toolkit==3.0.1",
"django-parler==2.3",
@@ -70,7 +71,6 @@ dependencies = [
[project.optional-dependencies]
dev = [
"django-extensions==4.1",
"drf-spectacular-sidecar==2025.6.1",
"ipdb==0.13.13",
"ipython==9.3.0",

63
src/frontend/Dockerfile Normal file
View File

@@ -0,0 +1,63 @@
FROM node:22-alpine AS frontend-deps
# Upgrade system packages to install security updates
RUN apk update && \
apk upgrade && \
rm -rf /var/cache/apk/*
WORKDIR /home/frontend/
COPY ./src/frontend/package.json ./package.json
COPY ./src/frontend/yarn.lock ./yarn.lock
COPY ./src/frontend/apps/desk/package.json ./apps/desk/package.json
COPY ./src/frontend/packages/i18n/package.json ./packages/i18n/package.json
COPY ./src/frontend/packages/eslint-config-people/package.json ./packages/eslint-config-people/package.json
RUN yarn install --frozen-lockfile
COPY .dockerignore ./.dockerignore
COPY ./src/frontend/.prettierrc.js ./.prettierrc.js
COPY ./src/frontend/packages/eslint-config-people ./packages/eslint-config-people
COPY ./src/frontend/apps/desk ./apps/desk
### ---- Front-end builder image ----
FROM frontend-deps AS frontend-base
WORKDIR /home/frontend/apps/desk
### ---- Front-end builder dev image ----
FROM frontend-deps AS frontend-dev
WORKDIR /home/frontend/apps/desk
EXPOSE 3000
CMD [ "yarn", "dev"]
FROM frontend-base AS frontend-builder
WORKDIR /home/frontend/apps/desk
ARG API_ORIGIN
ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
RUN yarn build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}
COPY --from=frontend-builder \
/home/frontend/apps/desk/out \
/usr/share/nginx/html
COPY ./src/frontend/apps/desk/conf/default.conf /etc/nginx/conf.d
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,4 +1,5 @@
server {
listen 3000;
listen 8080;
server_name localhost;
server_tokens off;

View File

@@ -97,6 +97,7 @@ test.describe('Mail domain', () => {
});
await page.goto('/mail-domains/unknown-domain.fr');
await page.waitForURL('/404/');
await expect(
page.getByText(
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',

View File

@@ -101,6 +101,8 @@ test.describe('Add Mail Domains', () => {
test('checks 404 on mail-domains/[slug] page', async ({ page }) => {
await page.goto('/mail-domains/unknown-domain');
await page.waitForURL('/404/');
await expect(
page.getByText(
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',

View File

@@ -83,6 +83,7 @@ test.describe('Teams Create', () => {
test('checks 404 on teams/[id] page', async ({ page }) => {
await page.goto('/teams/some-unknown-team');
await page.waitForURL('/404/');
await expect(
page.getByText(
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',

View File

@@ -32,12 +32,10 @@ export default defineConfig({
},
webServer: {
command: `cd ../.. && yarn app:${
process.env.CI ? 'start -p ' : 'dev --port '
} ${PORT}`,
command: !process.env.CI ? `cd ../.. && yarn app:dev --port ${PORT}` : '',
url: baseURL,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
reuseExistingServer: true,
},
/* Configure projects for major browsers */