Skip to main content

Command Palette

Search for a command to run...

Dockerize A React + Vite + Appwrite App

Updated
4 min read

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.