Warning
This guide has moved!
It is now maintained in the official Wasp docs: 👉 https://wasp.sh/docs/guides/deployment/caprover
This gist is archived and will no longer be updated.
Warning
This guide has moved!
It is now maintained in the official Wasp docs: 👉 https://wasp.sh/docs/guides/deployment/caprover
This gist is archived and will no longer be updated.
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:
It'll take you ~1 hour depending on your level of experience with servers and Github Actions.
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.
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.
PostgreSQL
myapp-db17postgresql://postgres:<password>@srv-captain--myapp-db:5432/postgres (srv-captain--myapp-db is based on the DB app name)myapp-server
https://api.<your-domain>Enable HTTPS for the domainContainer HTTP Port to 3001Force HTTPS by redirecting all HTTP traffic to HTTPS and Websocket SupportSave & Restart button after you change stuffmyapp-client
https://<your-domain>Enable HTTPS for the domainContainer HTTP Port to 8043Force HTTPS by redirecting all HTTP traffic to HTTPS and Websocket SupportSave & Restart button after you change stuffLet's go back into the server app and configure the required env vars:
App Configs and Environment VariablesDATABASE_URL with value of the database connection stringJWT_SECRET generate it with some online generatorPORT set it to 3001WASP_WEB_CLIENT_URL set it to https://<your-domain>WASP_SERVER_URL set it to https://api.<your-domain>.env.server file.github folder.github folder, create a new workflows folderdeploy.yml file from this gist to the workflows folderOnce you copy the deploy.yml, make sure to modify the:
WASP_VERSION env varSERVER_APP_NAME env var - this will be used in the Docker image nameSERVER_APP_URL env varCLIENT_APP_NAME env var - this will be used in the Docker image nameThe 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.
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:
CAPROVER_SERVER secret
https://captain.apps.mydomain.comSERVER_APP_TOKEN secret
server appDeployment find Method 1: Official CLIEnable App TokenCLIENT_APP_TOKEN secret
client appDeployment find Method 1: Official CLIEnable App TokenCluster in Caprover add a new Remote RegistryUsername to your Github usernamePassword to your Github tokenDomain to ghcr.ioImage Prefix to your Github usernameYou 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/deploy-from-github@v1.1.2 | |
| 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/deploy-from-github@v1.1.2 | |
| with: | |
| server: ${{ secrets.CAPROVER_SERVER }} | |
| app: ${{ env.CLIENT_APP_NAME }} | |
| token: ${{ secrets.CLIENT_APP_TOKEN }} | |
| image: ${{ steps.meta-client.outputs.tags }} |
For OpenSaaS, one thing I was missing was the database migration so I fixed by adding this step after 'Build Wasp App':
Add DATABASE_URL to Action secrets.
In CapRover, I also did the following for my database:
Http Settings
App Configs
@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
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;
};
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.