Skip to content

Instantly share code, notes, and snippets.

@infomiho
Last active April 24, 2025 15:39
Show Gist options
  • Save infomiho/a853e2f92aff6d52e9120b8974887464 to your computer and use it in GitHub Desktop.
Save infomiho/a853e2f92aff6d52e9120b8974887464 to your computer and use it in GitHub Desktop.
Deploy Wasp apps to Caprover

Self-hosting is a great way to have control over your deployment infrastructure (you own the data) and a great way to keep the costs down (you pay a fixed amount per month). One of the well established ways to manage your self-hosted deployment is Caprover.

Deploying Wasp apps to Caprover is straightforward:

  • create your Caprover apps (client, server and db)
  • build your app's Docker images (e.g. using Github Actions),
  • trigger Caprover to pull the Docker images and deploy them.

It'll take you ~1 hour depending on your level of experience with servers and Github Actions.

Installing Caprover

You should have Caprover installed and set up on your server. I'll be using Hetzner to rent my server.

Follow the Caprover install instructions: https://caprover.com/docs/get-started.html#prerequisites

Note

Following the install instructions, I've pointed an A record with value *.apps to my server IP. This gives me https://captain.apps.mydomain.com as my Caprover URL and enables me to have quick sub-domains i.e. https://<app-name>.apps.mydomain.com for testing stuff out.

Deploying your app

Adding your domain

To get Caprover apps working with your domain - you'll need to point a A record to your server IP:

  • To use myapp.com as your client domain, point the A record with the value of @ to your server IP.

  • To use api.myapp.com as your server domain, point the A record with the value of api to your server IP.

We'll set up the domains for our server and client apps below.

Create the Caprover apps

  1. Create a new one-click app, select PostgreSQL
    • Name it myapp-db
    • Enter the version to be one the latest version e.g. 17
    • Deploy it
    • Write down the connection string based on the info you get: postgresql://postgres:<password>@srv-captain--myapp-db:5432/postgres (srv-captain--myapp-db is based on the DB app name)
  2. Create a new app named myapp-server
    • Connect a new domain as https://api.<your-domain>
    • Press Enable HTTPS for the domain
    • Set the Container HTTP Port to 3001
    • Enable Force HTTPS by redirecting all HTTP traffic to HTTPS and Websocket Support
    • Make sure to hit the Save & Restart button after you change stuff
  3. Create a new app named myapp-client
    • Connect a new domain as https://<your-domain>
    • Press Enable HTTPS for the domain
    • Set the Container HTTP Port to 8043
    • Enable Force HTTPS by redirecting all HTTP traffic to HTTPS and Websocket Support
    • Make sure to hit the Save & Restart button after you change stuff

Configure Server Env Vars

Let's go back into the server app and configure the required env vars:

  1. Go under App Configs and Environment Variables
  2. Add the following env vars:
    • DATABASE_URL with value of the database connection string
    • JWT_SECRET generate it with some online generator
    • PORT set it to 3001
    • WASP_WEB_CLIENT_URL set it to https://<your-domain>
    • WASP_SERVER_URL set it to https://api.<your-domain>
  3. Add any other env vars you might have defined locally in the .env.server file

Add the Github Action

  1. In your app root dir create a new .github folder
  2. Inside of .github folder, create a new workflows folder
  3. Copy the deploy.yml file from this gist to the workflows folder

Configure the Github Action

Once you copy the deploy.yml, make sure to modify the:

  1. WASP_VERSION env var
  2. SERVER_APP_NAME env var - this will be used in the Docker image name
  3. SERVER_APP_URL env var
  4. CLIENT_APP_NAME env var - this will be used in the Docker image name

The DOCKER_REGISTRY, DOCKER_REGISTRY_USERNAME and DOCKER_REGISTRY_PASSWORD env vars will work out of the box for Github Container Registry.

Warning

If your app is located in the app folder (e.g. Open Saas has the app in the app folder) you can use the action as-is.

If your app is not in the app folder, follow the comments on lines 70, 87, 89, 98 and 100 to modify some paths.

Adding the repository secrets

The Github Action depends on some repository secrets to work properly - these are some values that can't be public. You add them by going into your repository Settings and then find Secrets and variables and select Actions.

Let's add the:

  1. CAPROVER_SERVER secret
    • Write your dashboard URL e.g. https://captain.apps.mydomain.com
  2. SERVER_APP_TOKEN secret
    • Go to your server app
    • Under Deployment find Method 1: Official CLI
    • Press Enable App Token
    • Paste it as the secret value
  3. CLIENT_APP_TOKEN secret
    • Go to your client app
    • Under Deployment find Method 1: Official CLI
    • Press Enable App Token
    • Paste it as the secret value

Enable Github Container Registry access

  1. Under Cluster in Caprover add a new Remote Registry
  2. Set Username to your Github username
  3. Set Password to your Github token
  4. Set Domain to ghcr.io
  5. Set Image Prefix to your Github username

Using Cloudflare (bonus)

You can move your domain's nameservers to Cloudflare to get the benefits of their CDN and DDoS protections.

I've had to keep my A record with the value *.apps as DNS Only since Cloudflare doesn't do multiple level free SSL. But I use Full SSL for the custom domains.

name: "Deploy"
on:
push:
branches:
- "main"
# This will make sure that only one deployment is running at a time
concurrency:
group: deployment
cancel-in-progress: true
env:
WASP_VERSION: "0.15.1"
# Put your server app name here
SERVER_APP_NAME: "pokemon-server"
# After you know the server URL, put the URL here
SERVER_APP_URL: "https://api.<your-domain>"
# Put your client app name here
CLIENT_APP_NAME: "pokemon-client"
DOCKER_REGISTRY: "ghcr.io"
DOCKER_REGISTRY_USERNAME: ${{ github.repository_owner }}
# This secret is provided by GitHub by default and is used to authenticate with the Container registry
DOCKER_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
jobs:
build-and-push-images:
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
# REMOVE this whole block if your app is not in the `app` folder
defaults:
run:
working-directory: ./app
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ env.DOCKER_REGISTRY_USERNAME }}
password: ${{ env.DOCKER_REGISTRY_PASSWORD }}
- name: (server) Extract metadata for Docker
id: meta-server
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_REGISTRY_USERNAME }}/${{ env.SERVER_APP_NAME }}
- name: (client) Extract metadata for Docker
id: meta-client
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_REGISTRY_USERNAME }}/${{ env.CLIENT_APP_NAME }}
- name: Install Wasp
shell: bash
run: curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v ${{ env.WASP_VERSION }}
- name: Build Wasp app
shell: bash
run: wasp build
- name: (client) Build
shell: bash
run: |
cd ./.wasp/build/web-app
REACT_APP_API_URL=${{ env.SERVER_APP_URL }} npm run build
- name: (client) Prepare the Dockerfile
shell: bash
run: |
cd ./.wasp/build/web-app
echo "FROM pierrezemb/gostatic" > Dockerfile
echo "CMD [\"-fallback\", \"index.html\", \"-enable-logging\"]" >> Dockerfile
echo "COPY ./build /srv/http" >> Dockerfile
- name: (server) Build and push Docker image
uses: docker/build-push-action@v6
with:
# REMOVE the `app` bit from the path if your app is not in the `app` folder
context: ./app/.wasp/build
# REMOVE the `app` bit from the path if your app is not in the `app` folder
file: ./app/.wasp/build/Dockerfile
push: true
tags: ${{ steps.meta-server.outputs.tags }}
labels: ${{ steps.meta-server.outputs.labels }}
- name: (client) Build and push Docker image
uses: docker/build-push-action@v6
with:
# REMOVE the `app` bit from the path if your app is not in the `app` folde
context: ./app/.wasp/build/web-app
# REMOVE the `app` bit from the path if your app is not in the `app` folder
file: ./app/.wasp/build/web-app/Dockerfile
push: true
tags: ${{ steps.meta-client.outputs.tags }}
labels: ${{ steps.meta-client.outputs.labels }}
- name: (server) Deploy to Caprover
uses: caprover/[email protected]
with:
server: ${{ secrets.CAPROVER_SERVER }}
app: ${{ env.SERVER_APP_NAME }}
token: ${{ secrets.SERVER_APP_TOKEN }}
image: ${{ steps.meta-server.outputs.tags }}
- name: (client) Deploy to Caprover
uses: caprover/[email protected]
with:
server: ${{ secrets.CAPROVER_SERVER }}
app: ${{ env.CLIENT_APP_NAME }}
token: ${{ secrets.CLIENT_APP_TOKEN }}
image: ${{ steps.meta-client.outputs.tags }}
@juan-ahv
Copy link

Looks like the .github/workflows/deploy.yml need to be in the root directory of the repository (where .git is located). Once that's moved the working directory needs to be updated from './app' to 'mySaaS/app' where mySaaS is your app's name. './app' needs to be replaced in all places.

@juan-ahv
Copy link

juan-ahv commented Jan 23, 2025

For OpenSaaS, one thing I was missing was the database migration so I fixed by adding this step after 'Build Wasp App':

  • name: (database) Migrate Prisma Schema
    shell: bash
    env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
    run: |
    export PATH="$HOME/.wasp/cli:$PATH"
    npx prisma migrate deploy

Add DATABASE_URL to Action secrets.

In CapRover, I also did the following for my database:

Http Settings

  • Expose externally and use the exposed url as the HOST in the DATABASE_URL secret
  • Change 'Container HTTP Port' to 5432 (just in case)

App Configs

  • Add port mapping: Server 5432 to Container 5432

@ABckh
Copy link

ABckh commented Mar 26, 2025

@juan-ahv Hi, wasp build already runs the migration, you just need to pass the DATBASE_URL to it:

      - name: Build Wasp app
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        shell: bash
        run: wasp build

@brahyam
Copy link

brahyam commented Apr 24, 2025

For me the Caprover option Enable Force HTTPS by redirecting all HTTP traffic to HTTPS and Websocket Support was causing redirect issues after deploying, once disabled the app worked properly but still was throwing CORS issues.

I solved them by adding:

// main.wasp
app OpenSaaS {
...
  server: {
    middlewareConfigFn: import { getGlobalMiddleware } from "@src/server/cors",
  },
...

and

// src/server/cors.ts

import cors from "cors";
import { MiddlewareConfigFn } from "wasp/server";


export const getGlobalMiddleware: MiddlewareConfigFn = (config) => {
  const isDevelopment = process.env.NODE_ENV === "development";
  console.log("isDevelopment", isDevelopment);
  const clientUrl = process.env.WASP_WEB_CLIENT_URL ?? "http://localhost:3000";
  const serverUrl = process.env.WASP_SERVER_URL ?? "http://localhost:3001";

  const origin = isDevelopment ? "*" : [clientUrl, serverUrl];


  config.delete("cors");
  config.set(
    "cors",
    cors({
      origin,
      credentials: true,
      methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
      allowedHeaders: [
        'Content-Type', 
        'Authorization', 
        'X-Requested-With',
        'Accept',
        'Origin'
      ],
      exposedHeaders: ['Access-Control-Allow-Origin'],
      preflightContinue: false,
      optionsSuccessStatus: 204      
    })
  );

  return config;
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment