A Better Deployment Process (Monorepos, CapRover, and Netlify)
• monorepo,, deployment,, caprover,, and netlify
Deployment, one of the first pipelines to set up and one of the most important pipelines to have. I recently set up a really handy deployment process with Github Actions and wanted to share more, for anyone who finds themselves in a similar situation. Here’s an overview of my situation:
- I have a monorepo, which makes deployment somewhat harder than I had hoped
- I wanted to deploy the backend to a private server with CapRover (this post will assume that CapRover is set up already)
- I wanted to deploy the frontend to Netlify, without building every time the backend updates
- I wanted the ability to deploy independently, together, manually, and automatically
For reference, I’ll share a link to my project so it’ll be easy to look up the finer points of my implementation.
A few problems I ran into upfront:
- CapRover makes deployment somewhat difficult if you have a monorepo, and I personally dislike vendor-specific files
- Netlify makes deployment somewhat difficult if you have a monorepo (as well), and I simply could not get their
build.ignoresettings to work, and the vendor-specific files too
So, first, lets work on the manual process for Netlify w/ GitHub Actions, since it’s so much easier. Thankfully, there’s a Netlify action that makes this easy:
- name: Deploy to Netlify uses: firstname.lastname@example.org with: publish-dir: './client/dist' production-branch: master github-token: $ deploy-message: "Deploy from GitHub Actions" enable-pull-request-comment: false enable-commit-comment: true overwrites-pull-request-comment: true env: NETLIFY_AUTH_TOKEN: $ NETLIFY_SITE_ID: $
Of course, this assumes the app is built to
client/dist first, but from there, it’s straightforward - turn off Netlify auto deploys, since this will cover it. Here’s my full action for client side manual deployments.
The server action, for me, was much more difficult since I’m not well-versed with Docker. First, I had to come up with a Dockerfile (this is the easiest way to deploy to CapRover, IMO). I had some issues trying to find one that would work with my old version of cWS, turns out I had to use node 13.14 specifically. But that’s neither here nor there, here’s the Dockerfile I came up with (current version here):
FROM node:13.14.0-alpine RUN apk add --no-cache libc6-compat RUN ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2 RUN mkdir -p /usr/src/app WORKDIR /usr/src/app ADD https://www.google.com /time.now COPY ./package.json /usr/src/app COPY ./package-lock.json /usr/src/app COPY ./tsconfig.json /usr/src/app COPY ./server /usr/src/app/server COPY ./shared /usr/src/app/shared RUN npm install RUN cd server && npm run setup && npm cache clean --force RUN cd server/content && npm install --unsafe-perm RUN cd server && npm run build ENV NODE_ENV production ENV PORT 80 EXPOSE 80 CMD cd server && npm start
I was running into some weird issues with needing a particular native function from
alpine doesn’t have. I searched around and found that I can symlink a different
.so file and make this work, so that was one problem sorted out.
After that, I ran into some issues installing a sub-repo that gets cloned, which threw some weird error that ended up being an npm issue, fixed by adding
--unsafe-perm to that npm install. Everything else is pretty straightforward for people who use docker, I imagine.
One might notice I structure things a bit differently than most. I think in other Dockerfiles I’ve seen copying a single
package.json file, installing, and going from there. Here, I had to copy
shared/, and the root
package.json file to get the install process correct.
From there, the action is really straightforward, but note that it must be run on linux. It looks like this:
- name: Deploy Server uses: AlexxNB/caprover-action@v1 with: server: 'https://captain.server.rair.land' password: '$' appname: 'game'
This action is yet a bit more convoluted because it does a build before it starts doing anything in the Dockerfile, then it does it again in the Dockerfile, but the time overhead isn’t significant enough for me to want to fix it. Here’s my full action for server deployments.
The two actions above handle the manual deployments, if needed. Seeing the full files shows how to set up manual deployments with GitHub Actions, but the tl;dr is
Now, that’s nice to have for testing, hotfixing, or whatever the case may be, but in general this is something worth automating. Thankfully, GitHub Actions is super flexible! It’s really easy to compose the two manual tasks above into something that only deploys on tags (the full action is here):
name: Release New Tags on: push: tags: - 'v*' # Any pushed tag jobs: build: name: Create Release runs-on: $ strategy: matrix: # os: [macos-latest, ubuntu-latest, windows-latest] os: [ubuntu-latest] node-version: [15.x] steps: - uses: actions/checkout@v2 - name: Use Node.js $ uses: actions/setup-node@v1 with: node-version: $ - run: npm install - run: npm run lint - run: npm run build - name: Deploy Server uses: AlexxNB/caprover-action@v1 with: server: 'https://captain.server.rair.land' password: '$' appname: 'game' - name: Deploy to Netlify uses: email@example.com with: publish-dir: './client/dist' production-branch: master github-token: $ deploy-message: "Deploy from GitHub Actions" enable-pull-request-comment: false enable-commit-comment: true overwrites-pull-request-comment: true env: NETLIFY_AUTH_TOKEN: $ NETLIFY_SITE_ID: $
This would automate client and server deployments on tags, but it could easily be changed to on push, or on any event GitHub Actions supports.