Dockerize A React + Vite + Appwrite App
Again, this is another quick note.
Instead of using Firebase, AWS and their confusing services, I wanted to use my own VPS for my app. Of course, in order to NOT deal with backend stuff (which is coming from a former backend dev), I wanted to set up Appwrite, which is a self-hosted BaaS (backend-as-a-service). Basically a Firebase alternative that you can set up on your own server.
What I like about it is:
It is almost the perfect BaaS. Covers tons of cases from Functions to Messaging to multiple projects.
It is relatively mature compared to other solutions.
So, let’s get started.
This post already assumes you have created a fresh new Vite+React project.
Step 1: Dockerizing Our Project
We need a multi-stage Dockerfile to build and then serve our Vite project. Here it is:
# Stage 1: Build
FROM node:20-alpine AS build # Define "build" stage
WORKDIR /app # set working dir
COPY package.json package-lock.json ./ # copy dep files
RUN corepack enable && npm i # install deps
COPY . . # copy everything else
ENV NODE_ENV=production # set env to production
RUN npm build # start building
# Stage 2: Run
FROM nginx:alpine # Define "run" stage
COPY --from=build /app/dist /usr/share/nginx/html # Copy transpiled files from "build" stage to this container
COPY ./nginx.conf /etc/nginx/conf.d/default.conf # add our custom nginx config
EXPOSE 80 # expose port 8*
CMD ["nginx", "-g", "daemon off;"] # define run script
So, we also need to create two more things. The first one is defined in Dockerfile, it is our custom nginx.conf file.
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# SPA support - redirect all routes to index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
}
And also we need to ignore some files for Docker. Create a .dockerignore with this content:
node_modules
npm-debug.log
.git
.gitignore
README.md
.eslintrc
.prettierrc
Dockerfile
.dockerignore
And run with:
docker build -t ourprojectname .
If it runs successfully, that is fine.
Step 2: Connecting It Altogether with Appwrite
Now, we need to connect our app to Appwrite, which we will define with Docker Compose. Here is my docker-compose.yml file:
version: "3.8"
services:
ourprojectname: # change this
build:
context: .
args:
- VITE_APPWRITE_ENDPOINT=${VITE_APPWRITE_ENDPOINT}
- VITE_APPWRITE_PROJECT=${VITE_APPWRITE_PROJECT}
ports:
- "80:80"
depends_on:
- appwrite
environment:
- VITE_APPWRITE_ENDPOINT=${VITE_APPWRITE_ENDPOINT}
- VITE_APPWRITE_PROJECT=${VITE_APPWRITE_PROJECT}
networks:
- appwrite-network
env_file:
- .env.app # we will create this file
# AppWrite services
appwrite:
image: appwrite/appwrite:latest
depends_on:
- mariadb # for appwrite to store data
- redis # for appwrite to cache data
ports:
- "1900:80" # host machine port is 1900 because our app uses 80
volumes: # define volumes on host machine to store data somewhere
# change "ourprojectname"
- ourprojectname-appwrite-uploads:/storage/uploads
- ourprojectname-appwrite-cache:/storage/cache
- ourprojectname-appwrite-config:/storage/config
- ourprojectname-appwrite-certificates:/storage/certificates
- ourprojectname-appwrite-functions:/storage/functions
networks:
- appwrite-network
env_file:
- .env.appwrite # we will create this file
mariadb:
image: mariadb:12
volumes: # define volumes on host machine to store data somewhere
# change "ourprojectname"
- ourprojectname-mariadb-data:/var/lib/mysql
networks:
- appwrite-network
env_file:
- .env.mariadb # we will create this file
redis:
image: redis:8
volumes: # define volumes on host machine to store data somewhere
# change "ourprojectname"
- ourprojectname-redis-data:/data
networks:
- appwrite-network
volumes:
# all our volumes
ourprojectname-appwrite-uploads:
ourprojectname-appwrite-cache:
ourprojectname-appwrite-config:
ourprojectname-appwrite-certificates:
ourprojectname-appwrite-functions:
ourprojectname-mariadb-data:
ourprojectname-redis-data:
networks:
appwrite-network:
driver: bridge
So, what this does is, it sets up Appwrite container, and then Redis and MariaDB containers because Appwrite needs them to store some data.
However, as you can see, we need to define a lot of .env files. Of course, these .env files will contain sensitive data, so I will ignore them in .gitignore like this:
# ... other entries ...
.env
.env.*
!.env*example
You might have noticed, I excluded .env.*.example files, because we will use them as template. So, let’s create them:
# .env.app.example file
VITE_APPWRITE_ENDPOINT=http://localhost/v1
VITE_APPWRITE_PROJECT=band9buddy
# .env.appwrite.example file
_APP_ENV=production
_APP_OPENSSL_KEY_V1=your-very-secure-encryption-key-here
_APP_DOMAIN=localhost
_APP_REDIS_HOST=redis
_APP_DB_HOST=mariadb
_APP_DB_USER=appwrite-user
_APP_DB_PASS=super-secure-password-456
_APP_DB_SCHEMA=appwrite
_APP_STORAGE_DEVICE=local
_APP_STORAGE_ANTIVIRUS=clamav
# .env.mariadb.example file
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=appwrite
MYSQL_USER=user
MYSQL_PASSWORD=password
And copy and paste them and remove .example part from the filename. These examples will stay as a template in our repo for setting it up later on the server.
Now, if we run:
docker compose up # -d for detaching
We will see it runs just fine.

