Racket Deployment

March 8, 2025

Overview

The following systems/components/tooling are required:

  • Ubuntu VPS: EC2 instance
  • Web Application Project: Racket Project or Racket Executable
  • Reverse Proxy: Nginx
  • DNS management software: Cloudflare
  • SSL Certification Program: Certbot

Ubuntu VPS

  • Ensure you have root access.
  • Configure SSH Access.
  • Note the host's public IP.

Project Configuration

  • What port is the application listening on?

    The convention for web applications is to listen on 8000, but you can configure this.

    The most important thing is to specify in the reverse proxy configuration where the web application will be listening.

  • Where are the static files?

Reverse Proxy Configuration

Nginx

What are the configuration files?

  • /etc/nginx/sites-available/ayoonipe.com
  • /etc/nginx/sites-enabled/ayoonipe.com

This is what the sites-available config should look like:

server {
    listen 80;
    server_name ayoonipe.com www.ayoonipe.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name ayoonipe.com www.ayoonipe.com;

    location / {
        proxy_pass http://localhost:8000;  # Ensure your Racket app is running on this port
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Where the configuration in sites-enabled is a symbolic link of sites-available:

sudo ln -s /etc/nginx/sites-available/ayoonipe.com /etc/nginx/sites-enabled/

DNS Configuration

Cloudflare

Using DNS management console for the domain your site will be hosted on, add an A record for the name, content, and proxy-status [: DNS only].

  • Add A record for www.ayoonipe.com

    name: www content: 3.84.189.64 proxy-status: DNS-only

  • Add A record for ayoonipe.com

    name: @ content: 3.84.189.64 proxy-status: DNS-only

  • Why DNS only?

    I have not yet bothered with investigating this but having proxied records makes the site unreachable.

    This is what you want, but proxied records, points to a different IP than the content. I know, this is what a "proxy" does, but you sort of expect it to just work you know…

    $ nslookup www.ayoonipe.com
    Server:     2001:568:ff09:10c::67
    Address:    2001:568:ff09:10c::67#53
    
    Non-authoritative answer:
    Name:   www.ayoonipe.com
    Address: 3.84.189.64
    
    $ nslookup ayoonipe.com
    Server:     2001:568:ff09:10c::67
    Address:    2001:568:ff09:10c::67#53
    
    Non-authoritative answer:
    Name:   ayoonipe.com
    Address: 3.84.189.64

SSL Certification

Certbot

To get SSL certification for your site using certbot:

sudo certbot --nginx -d ayoonipe.com -d www.ayoonipe.com

Effect: nginx site config has entries that are managed by certbot.

Condition: This only works properly if config file-name is the same as the domain of the site you are certifying.

server {        
    if ($host = www.ayoonipe.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    if ($host = ayoonipe.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    server_name ayoonipe.com www.ayoonipe.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name ayoonipe.com www.ayoonipe.com;
    ssl_certificate /etc/letsencrypt/live/ayoonipe.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/ayoonipe.com/privkey.pem; # managed by Certbot

    location / {
        proxy_pass http://localhost:8000;  # Ensure your Racket app is running on this port
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Systemd

You will want to turn your application into a systemd service to ensure:

  • auto-restart on failure
  • automatic startup on boot
  • easier management with systemctl start/stop/restart

Add your project as a service definition in the systemd service directory. Save as /etc/systemd/system/ayoonipe.com/service.

[Unit]
Description=Racket Web Application
After=network.target

[Service]
ExecStart=/var/www/ayoonipe/dist/bin/app
Restart=always
User=ubuntu
WorkingDirectory=/var/www/ayoonipe/dist
StandardOutput=append:/var/log/ayoonipe.com.log
StandardError=append:/var/log/ayoonipe.com.log          

[Install]
WantedBy=multi-user.target      

Then enable and start the service. The following excerpt also shows how to get the status of the service.

ubuntu@ip-172-31-91-97:/var/www/ayo-onipe$ sudo systemctl daemon-reload
ubuntu@ip-172-31-91-97:/var/www/ayo-onipe$ sudo systemctl enable ayoonipe.com
ubuntu@ip-172-31-91-97:/var/www/ayo-onipe$ sudo systemctl start ayoonipe.com
ubuntu@ip-172-31-91-97:/var/www/ayo-onipe$ sudo systemctl status ayoonipe.com
 ayoonipe.com.service - Racket Web Application
     Loaded: loaded (/etc/systemd/system/ayoonipe.com.service; enabled; preset: enabled)
     Active: active (running) since Fri 2025-02-28 22:16:21 UTC; 31s ago
   Main PID: 23007 (racketcs-8.10)
      Tasks: 2 (limit: 1129)
     Memory: 170.1M (peak: 200.6M)
        CPU: 625ms
     CGroup: /system.slice/ayoonipe.com.service
             └─23007 /var/www/ayo-onipe/dist/bin/../lib/plt/racketcs-8.10 -E /var/www/ayo-onipe/dist/bin/app -N /var/www>

Feb 28 22:16:21 ip-172-31-91-97 systemd[1]: Started ayoonipe.com.service - Racket Web Application.
Feb 28 22:16:21 ip-172-31-91-97 app[23007]: Your Web application is running at http://localhost:8000.
Feb 28 22:16:21 ip-172-31-91-97 app[23007]: Stop this program at any time to terminate the Web Server.

CI/CD

Github

For this particular deployment workflow, there are 2 stages:

  • build: builds the racket app artifact and uploads it for the deployment.

  • deploy:

    distributes the artifact to EC2 instance and configures proper permissions so that it can be run (as service) without issues.

Github allows deployment instructions to be written as yaml files, and saved at .github/workflows/deploy.yml for runners/workers to pick up.

Ensure that you have set up Access Keys as Github Secrets. The runner/worker will use this to access the EC2 instance and do work.

EC2_HOST: ${{ secrets.EC2_IP }}
SSH_KEY: ${{ secrets.EC2_SSH_KEY }}

Ensure that the executables and runtimes in the distributed artifact (i.e dist) have execute permission. This can be automated, check out the deploy.yml.

The deploy.yml:

name: Deploy Racket App

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Install Racket
        run: sudo apt update && sudo apt install racket -y

      - name: Build Racket Distribution
        run: |
          raco exe -o app app.rkt
          raco distribute dist app

      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: racket-distribution
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest

    steps:
      - name: Download Artifact
        uses: actions/download-artifact@v4
        with:
          name: racket-distribution
          path: dist/

      - name: Deploy to EC2
        env:
          EC2_HOST: ${{ secrets.EC2_IP }}
          SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
          USER: "ubuntu"
          DEPLOY_DIR: "/var/www/ayo-onipe"
          SERVICE_NAME: "ayoonipe.com.service"
        run: |
          echo "$SSH_KEY" > ssh_key.pem
          chmod 600 ssh_key.pem

          # Stop existing service
          ssh -o StrictHostKeyChecking=no -i ssh_key.pem $USER@$EC2_HOST "sudo systemctl stop $SERVICE_NAME"

          # Ensure the deployment directory exists
          ssh -o StrictHostKeyChecking=no -i ssh_key.pem $USER@$EC2_HOST "sudo mkdir -p $DEPLOY_DIR && sudo chown $USER:$USER $DEPLOY_DIR"

          # Sync new distribution to /var/www/ayo-onipe/
          rsync -avz -e "ssh -i ssh_key.pem -o StrictHostKeyChecking=no" dist/ $USER@$EC2_HOST:$DEPLOY_DIR/dist/

          # Add execute permission in distributed artifact
          ssh -o StrictHostKeyChecking=no -i ssh_key.pem $USER@$EC2_HOST "chmod +x $DEPLOY_DIR/dist/bin/app && chmod -R +x $DEPLOY_DIR/dist/lib/plt"

          # Restart the service
          ssh -o StrictHostKeyChecking=no -i ssh_key.pem $USER@$EC2_HOST "sudo systemctl restart $SERVICE_NAME"

      - name: Cleanup
        run: rm -f ssh_key.pem

<-- Back to Index.