Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add production dockerfiles and github action #794

Merged
merged 3 commits into from
Feb 12, 2024
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
57 changes: 57 additions & 0 deletions .github/workflows/build-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Build images

on:
push:
branches: [main]

env:
REGISTRY: ghcr.io
FRONTEND_IMAGE_NAME: igalia/phpreport/frontend
API_IMAGE_NAME: igalia/phpreport/api

jobs:
build_and_push_image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker (frontend)
id: meta-frontend
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}
- name: Build and push Docker image (frontend)
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod.frontend.Dockerfile
push: true
pull: true
tags: |
ghcr.io/igalia/phpreport/frontend:latest
labels: ${{ steps.meta-frontend.outputs.labels }}
- name: Extract metadata (tags, labels) for Docker (api)
id: meta-api
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.API_IMAGE_NAME }}
- name: Build and push Docker image (api)
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod.api.Dockerfile
push: true
pull: true
tags: |
ghcr.io/igalia/phpreport/api:latest
labels: ${{ steps.meta-api.outputs.labels }}

37 changes: 37 additions & 0 deletions docker/docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
version: "3"
services:
db:
image: postgres:13
container_name: phpreport-db
ports:
- "5432:5432"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this port need to be exposed at all? Is it intended to be used by anything besides api and frontend?

volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: phpreport
POSTGRES_USER: phpreport
POSTGRES_DB: phpreport
api:
build:
context: ../
dockerfile: docker/prod.api.Dockerfile
container_name: phpreport-api
env_file:
- ../.env
ports:
- "8555:8555"
depends_on:
- db
frontend:
build:
context: ../
dockerfile: docker/prod.frontend.Dockerfile
container_name: phpreport-frontend
env_file:
- ../frontend/.env.local
ports:
- "3000:3000"
depends_on:
- api
volumes:
pgdata:
17 changes: 17 additions & 0 deletions docker/prod.api.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM debian:bullseye-slim

RUN apt update && apt -y install python3 python3-pip openssh-client

WORKDIR /api

COPY ./api/pyproject.toml /api/pyproject.toml

RUN python3 -m pip install --upgrade pip
RUN pip install --no-cache-dir .
RUN pip install .[dev]

COPY ./api /api

EXPOSE 8555

CMD ["uvicorn", "main:app","--proxy-headers", "--reload", "--host", "0.0.0.0", "--port", "8555"]
46 changes: 46 additions & 0 deletions docker/prod.frontend.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
FROM node:lts-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat

WORKDIR /frontend
COPY frontend/package*.json /frontend

RUN npm ci

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /frontend

COPY --from=deps /frontend/node_modules ./node_modules
COPY frontend /frontend

ENV NEXT_TELEMETRY_DISABLED 1

RUN npm run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /frontend

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

COPY --from=builder --chown=nextjs:nodejs /frontend/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /frontend/.next/static ./.next/static

USER nextjs

EXPOSE 3000

CMD ["node", "server.js"]

1 change: 1 addition & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
async redirects() {
return [
{
Expand Down
35 changes: 18 additions & 17 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@
"@mui/joy": "^5.0.0-beta.20",
"@tanstack/react-query": "^4.35.3",
"@testing-library/user-event": "^14.5.1",
"@types/node": "20.6.0",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"@types/react": "18.2.23",
"@types/react-dom": "18.2.8",
"date-fns": "^2.30.0",
"eslint": "8.49.0",
"eslint-config-next": "^14.0.4",
Expand Down
101 changes: 101 additions & 0 deletions frontend/src/app/api/auth/[...nextauth]/authOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { NextAuthOptions } from 'next-auth'
import KeycloakProvider from 'next-auth/providers/keycloak'
import { fetchFactory } from '@/infra/lib/apiClient'
import { makeGetCurrentUser } from '@/infra/user/getCurrentUser'
import { JWT } from 'next-auth/jwt'

/**
* Takes a token, and returns a new token with updated
* `accessToken` and `accessTokenExpires`. If an error occurs,
* returns the old token and an error property
*/
async function refreshAccessToken(token: JWT) {
try {
const url = `${process.env.OIDC_TOKEN_ENDPOINT}`

const params = {
grant_type: 'refresh_token',
client_id: process.env.OIDC_CLIENT_ID!,
client_secret: process.env.OIDC_CLIENT_SECRET!,
refresh_token: token.refreshToken!
}

const response = await fetch(url, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(params),
method: 'POST'
})

const refreshedTokens = await response.json()

if (!response.ok) {
throw refreshedTokens
}

return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken // Fall back to old refresh token
}
} catch (error) {
return {
...token,
error: 'RefreshAccessTokenError'
}
}
}

export const authOptions: NextAuthOptions = {
providers: [
KeycloakProvider({
clientId: process.env.OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET!,
issuer: process.env.OIDC_AUTHORITY
})
],
pages: {
error: '/auth/error'
},
callbacks: {
async redirect({ baseUrl }) {
return baseUrl
},
async session({ session, token }) {
session.accessToken = token.accessToken
session.user = { ...session.user, ...token.user }
session.accessTokenExpires = token.accessTokenExpires
session.refreshToken = token.refreshToken

return session
},
async jwt({ token, account, profile, trigger }) {
if (trigger === 'update' && Date.now() > token.accessTokenExpires!) {
const newToken = await refreshAccessToken(token)

return newToken
}

if (account && profile) {
token.accessToken = account.access_token
token.accessTokenExpires = account.expires_at * 1000
token.refreshToken = account.refresh_token
token.id = profile.id

const apiClient = fetchFactory({ baseURL: process.env.API_BASE!, token: token.accessToken })
const getCurrentUser = makeGetCurrentUser(apiClient)


const user = await getCurrentUser()

token.user = user
}

return token
}
}
}


Loading
Loading