Racket Deployment
March 8, 2025Overview
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