
Deploying an Advanced CMS with Astro and Strapi Using Portainer and PostgreSQL
Blog
I decided to migrate my deployments from native environments to containerization. I had numerous projects deployed, and the new ones had conflicting requirements. Additionally, backup and recovery became much simpler when the application is defined in a single docker-compose file.
In this guide, I'll explain how to deploy Astro and Strapi using PostgreSQL to create your new advanced CMS site—all in one comprehensive tutorial. I spent an embarrassingly long time figuring this out, but it's not entirely my fault (as I'll explain below).
The stack:
- Astro Server Side Rendered (SSR)
- Strapi 4
- Postgress 12
- Nginx
- Portainer using GitOps
Create a repository to host your code
Choose any repository hosting service you'd like. I host mine on my own GitLab server. This will enable us to automatically deploy the application when a new push is made using Portainer's GitOps capabilities.
Folder Structure:
- Frontend (Astro): Root directory (/)
- Backend (Strapi): /server
Note: In retrospect, I would have placed the Astro frontend in its own folder as well.
Declare Dockerfiles for Both Server and Client
Astro Dockerfile
This Dockerfile is located in the root directory and is responsible for creating the Astro Docker image.
FROM node:18-alpine AS base
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn build
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD node ./dist/server/entry.mjs
Strapi Dockerfile
This Dockerfile is located in the server directory and is responsible for creating the Strapi Docker image.
Notes:
- strapi-db-data-dbdump is not needed for the first deployment. It's useful if you're migrating the project and want to restore the database from a SQL dump file.
- strapi-uploads: Separating Strapi's uploads directory ensures uploaded content remains safe during redeployments.
# Build stage
FROM node:18-alpine
# Installing libvips-dev for sharp Compatibility
RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev nasm bash vips-dev
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}
WORKDIR /opt/app
# Copy package.json and yarn.lock
COPY package.json yarn.lock ./
# Clean Yarn cache and install dependencies
RUN yarn cache clean && yarn install --frozen-lockfile --verbose
COPY . .
# Build admin panel
RUN yarn build
# Expose port and run the app
EXPOSE 1337
CMD ["yarn", "start"]
Create the Docker Compose File
Create a docker-compose.yml file in the root directory:
services:
strapiDB:
restart: unless-stopped
image: postgres:12.0-alpine
platform: linux/amd64
environment:
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
volumes:
- strapi-db-data:/var/lib/postgresql/data/
- strapi-db-data-dbdump:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
networks:
- luke-iremadze-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USERNAME} -d ${DATABASE_NAME}"]
interval: 10s
timeout: 5s
retries: 5
strapiAdminer:
image: adminer
restart: unless-stopped
ports:
- "9090:8080"
environment:
- ADMINER_DEFAULT_SERVER=strapiDB
networks:
- luke-iremadze-network
depends_on:
- strapiDB
backend-strapi:
build:
context: ./server
dockerfile: Dockerfile
pull_policy: build
depends_on:
strapiDB:
condition: service_healthy
restart: unless-stopped
environment:
DATABASE_CLIENT: ${DATABASE_CLIENT}
DATABASE_HOST: ${DATABASE_HOST}
DATABASE_PORT: ${DATABASE_PORT}
DATABASE_NAME: ${DATABASE_NAME}
DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET}
APP_KEYS: ${APP_KEYS}
NODE_ENV: ${NODE_ENV}
API_TOKEN_SALT: ${API_TOKEN_SALT}
TRANSFER_TOKEN_SALT: ${TRANSFER_TOKEN_SALT}
PUBLIC_URL: ${PUBLIC_URL}
volumes:
- strapi-data:/opt/app
- strapi-uploads:/opt/app/public/uploads
ports:
- "1337:1337"
networks:
- luke-iremadze-network
frontend:
build:
context: . # Path to Astro frontend project
dockerfile: Dockerfile
pull_policy: build
volumes:
- astro-static:/app/dist
ports:
- "4321:4321"
networks:
- luke-iremadze-network
webserver:
depends_on:
- frontend
- backend-strapi
image: nginx:stable-alpine
restart: unless-stopped
ports:
- "8295:80"
volumes:
- /srv/${PROJECT_NAME}/nginx/default.conf:/etc/nginx/conf.d/default.conf
- astro-static:/usr/share/nginx/html
- strapi-data:/opt/app
- strapi-uploads:/opt/app/public/uploads
networks:
- luke-iremadze-network
volumes:
strapi-db-data:
driver: local
driver_opts:
type: none
o: bind
device: /srv/${PROJECT_NAME}/pg-data
strapi-db-data-dbdump:
driver: local
driver_opts:
type: none
o: bind
device: /srv/${PROJECT_NAME}/dump
strapi-data:
driver: local
driver_opts:
type: none
o: bind
device: /srv/${PROJECT_NAME}/strapi
strapi-uploads:
driver: local
driver_opts:
type: none
o: bind
device: /srv/${PROJECT_NAME}/uploads
astro-static: # Defined the volume for static assets
driver: local
driver_opts:
type: none
o: bind
device: /srv/${PROJECT_NAME}/public_html
networks:
luke-iremadze-network:
driver: bridge
Notes:
- Replace ${PROJECT_NAME} with your actual project name.
- Ensure all environment variables are defined in an .env file or within your deployment platform.
- Pre-create the folders for the volumes to mount
Nginx Default Configuration
Create the default.conf file in /srv/${PROJECT_NAME}/nginx/default.conf.
# Proxy for Strapi (api.iremadze.com)
server {
listen 80;
server_name api.iremadze.com;
location / {
proxy_pass http://backend-strapi:1337; # Strapi backend service
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $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 Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass_request_headers on;
}
}
# Proxy for Astro (luke.iremadze.com)
server {
listen 80;
server_name luke.iremadze.com;
location / {
proxy_pass http://frontend:4321; # Astro frontend service
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;
}
}
Note: Replace api.example.com and www.example.com with your actual domain names.
Add the Repository to Portainer
Step 1: Create a new stack in Portainer and add the repository address.
Step 2: Select the appropriate settings to enable auto-updates.
Step 3: Declare the environment variables.
Environment Variables:
DATABASE_USERNAME=strapi-db-user
DATABASE_PASSWORD=PASSWORD
DATABASE_NAME=strapi-db
DATABASE_PORT=5432
DATABASE_CLIENT=postgres
JWT_SECRET=SDFGWERG@
ADMIN_JWT_SECRET=@#F@#FE@RF@R
APP_KEYS=DFGSDFG,SDFGDFG,SDFGSDFG,QWREFGF==
NODE_ENV=production
PROJECT_NAME=lukes-landing
DATABASE_HOST=strapiDB
API_TOKEN_SALT=1242352FD@
TRANSFER_TOKEN_SALT=SFGSG#G#
PUBLIC_URL=https://api.iremadze.com/
Errors I Faced and My Resolutions
The db.config.connection is undefined error
I spent an embarrassing amount of time troubleshooting the error:
TypeError: Cannot destructure property 'client' of 'db.config.connection' as it is undefined
Cause: This issue is related to TypeScript and its behavior within Docker.
Resolution: Remove the noEmit rule from your tsconfig.json. Here's the configuration I used, thanks to tirthshah574:
{
"extends": "@strapi/typescript-utils/tsconfigs/server",
"compilerOptions": {
"outDir": "dist",
"rootDir": ".",
"lib": ["esnext", "dom"],
"skipLibCheck": true, // Skip type checking of declaration files
"strict": false, // Disable strict type checking
"allowJs": true, // Allow JavaScript files to be compiled
"checkJs": false, // Disable checking JavaScript files
"noImplicitAny": false,
"noImplicitThis": false
},
"include": [
"./",
"./**/*.ts",
"./**/*.js",
"src/**/*.json",
],
"exclude": [
"node_modules/",
"build/",
"dist/",
".cache/",
".tmp/",
"src/admin/",
"**/*.test.ts",
"src/plugins/**",
"types/generated/**"
]
}
Updates Don't Reflect After Redeployment
After updating the settings, I noticed that changes weren't reflecting upon redeployment.
Cause: The new image deployment wasn't overwriting existing files in the volumes, causing the application to use older versions.
Resolution: Manually remove the strapi and public_html folders from the project directory on the server before redeploying via Portainer. This forces the containers to use the updated images.
Final thoughts
By following this guide, you should have a fully functional CMS setup using Astro and Strapi, managed through Docker Compose and Portainer. This approach simplifies dependency management, resolves conflicts, and streamlines backup and recovery processes.
I will be on the lookout to automate the contents of the project being deleted and recreated to be fully automatic, but in the mean time I'll make due with this.
Feel free to share your experiences or ask questions in the comments below!