I know only R, how do I set up my own server and my own website ?
My goal is to have the flexibility to create public facing services and personal projects using a permanent running server.
I already know I want to use freely any language, any DB, any API. I want to be able to code on server side and build shiny apps or REST apis of my own, secure access to some pages etc.
Let’s describe first how we got to put this page online, and maybe we’ll discuss fancier things later.
Again I’m no authority, really learning as I type.
We don’t need any provider to share a page or a service online, we can do it from our own computer.
Indeed we have an IP adress, we can open ports and exchange through those, but :
To solve the first 2 points we need a host, to solve the last one we need a domain. Many hosts provide a domain if we subscribe.
There are 3 main ways to go for webhosting, shared hosting, VPS, or standalone servers.
According to this comparative by LWS VPS seems to be the way for the flexibility I need.
LWS is a french provider with great reviews a generous offer for 10 euros per month (without any scammy price after the 1st month) and the domain name is offered.
We have one choice to make regarding the OS and preinstallation. To be ultra flexible chatGPT tells me to choose Debian 12 + SSH (not the default choice), I’ll follow along.
I’ll go with [m4i.be] for the domain name, 3 letters name and the url looks like “maybe”.
It is straightforward to pay easily through my bank app, and a few hours later the account is ready.
Once I receive confirmation that it is setup I go the LWS domain management panel at https://panel.lws.fr/, choose the VPS service at the top, then click on the SSH keys icon to set the SSH connection. I give the name Antoine1 to my connection, then locally in my terminal I run :
cat ~/.ssh/id_rsa.pub
This retrieves the SSH key I had already generated on my laptop. I
then copy and paste the output (ssh-rsa ...) the LWS
form.
I validate my choice and reach a screen where it tells me that i have to wait for my key to be registered, it actually just takes a few minutes.
Locally in my terminal I can now run
ssh root@31.207.34.184 and the following commands will be
run on the server.
There I update my system and install required tools
apt update && apt upgrade -y
base dependencies, a bunch of common system tools that we’ll need directly or through R:
apt install -y sudo wget curl libcurl4-openssl-dev gnupg lsb-release software-properties-common apt-transport-https gdebi-core git ufw locales systemd rsync net-tools python3 python3-pip nginx libsqlite3-dev pandoc libfontconfig1-dev libfreetype6-dev libfreetype6-dev libfribidi-dev libharfbuzz-dev libxml-2.0
ufw allow 22
ufw allow 3838 # Shiny
ufw allow 8080 # VS Code Server
ufw allow 80 # HTTP (nginx)
ufw allow 443 # HTTPS (quand tu installeras certbot)
ufw --force enable
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 'E298A3A825C0D65DFD57CBB651716619E084DAB9'
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B8F25A8A73EACF41
echo "deb https://cloud.r-project.org/bin/linux/debian bookworm-cran40/" > /etc/apt/sources.list.d/cran.list
apt update
apt install -y r-base
Packages that need compilation can take forever to install from CRAN, but we can use the Posit package manager when our distribution is supported, it also allows us to use a snapshot date, which will go a long way for reproducibility.
The pak package tells us what is available :
# all supported distributions
pak:::ppm_platforms_internal() |> print(n = Inf)
## # A data frame: 32 × 6
## name os binary_url distribution release binaries
## <chr> <chr> <chr> <chr> <chr> <lgl>
## 1 centos7 linux centos7 centos 7 TRUE
## 2 centos8 linux centos8 centos 8 TRUE
## 3 rhel9 linux rhel9 rockylinux 9 TRUE
## 4 opensuse15 linux opensuse15 opensuse 15 TRUE
## 5 opensuse152 linux opensuse152 opensuse 15.2 TRUE
## 6 opensuse153 linux opensuse153 opensuse 15.3 TRUE
## 7 opensuse154 linux opensuse154 opensuse 15.4 TRUE
## 8 opensuse155 linux opensuse155 opensuse 15.5 TRUE
## 9 opensuse156 linux opensuse156 opensuse 15.6 TRUE
## 10 opensuse42 linux opensuse42 opensuse 42.3 TRUE
## 11 rhel7 linux centos7 redhat 7 TRUE
## 12 rhel8 linux centos8 redhat 8 TRUE
## 13 rhel9 (unused alias) linux rhel9 redhat 9 TRUE
## 14 sles12 linux opensuse42 sle 12.3 TRUE
## 15 sles15 linux opensuse15 sle 15 TRUE
## 16 sles152 linux opensuse152 sle 15.2 TRUE
## 17 sles153 linux opensuse153 sle 15.3 TRUE
## 18 sles154 linux opensuse154 sle 15.4 TRUE
## 19 sles155 linux opensuse155 sle 15.5 TRUE
## 20 sles156 linux opensuse156 sle 15.6 TRUE
## 21 xenial linux xenial ubuntu 16.04 TRUE
## 22 bionic linux bionic ubuntu 18.04 TRUE
## 23 focal linux focal ubuntu 20.04 TRUE
## 24 jammy linux jammy ubuntu 22.04 TRUE
## 25 noble linux noble ubuntu 24.04 TRUE
## 26 buster linux buster debian 10 FALSE
## 27 bullseye linux bullseye debian 11 TRUE
## 28 bookworm linux bookworm debian 12 TRUE
## 29 windows windows <NA> windows all TRUE
## 30 macos macos <NA> macos all TRUE
## 31 manylinux_2_28 linux manylinux_2_28 centos 8 TRUE
## 32 internal linux internal internal all TRUE
We see that there is one option for Debian 12, named “bookworm”.
And pak:::ppm_snapshots_internal() |> tail(1) gives
use the last available snapshot, “2025-05-05” at time of writing.
https://pak.r-lib.org/reference/ppm_snapshots.html tells us how to format the repos url we need.
We type R in the terminal and then the following.
options(repos = "https://packagemanager.posit.co/cran/__linux__/bookworm/2025-05-05")
install.packages("pak")
pak::pak(c(
"arrow", "bench", "constructive", "covr", "data.table", "devtools", "DT", "here",
"httr", "httr2", "igraph", "knitr", "languageserver", "lintr", "lubridate",
"pagedown", "pkgcache", "pkgdown", "profvis", "reactable", "readxl", "renv",
"reticulate", "rmarkdown", "rstudioapi", "rstudioapi", "rvest",
"shiny", "styler", "testthat", "tidymodels", "tidyverse", "tinytex",
"usethis"
))
The q() to exit R
We can use vs code in 2 ways, either use VS Code Server through our web browser, or use VS Code on our laptop, connected through SSH.
The latter is generally more convenient for a single user always working from their own laptop, but it’s useful to have an instance available from anywhere. So let’s do it first.
curl -fsSL https://code-server.dev/install.sh | sh
Then we need to set a password, we call first:
mkdir -p ~/.config/code-server
nano ~/.config/code-server/config.yaml
Then make sure it looks like this:
bind-addr: 0.0.0.0:8080
auth: password
password: replace-this-with-your-password
cert: false
Create a systemd service for VS Code Server
cat <<EOF > /etc/systemd/system/code-server.service
[Unit]
Description=code-server
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/code-server
Restart=always
[Install]
WantedBy=multi-user.target
EOF
And run the following:
systemctl daemon-reload
systemctl enable code-server
systemctl start code-server
Then VS code is accessible at this adress: http://31.207.34.184:8080/ (We’ll need to type our password there)
It seems to be common knowledge that VS Code and Safari don’t play well together, I found at least that copy and paste is broken, so we switched to Chrome and everything’s fine.
In the left side bar we find the extension icon and we lookup and install “R Extension for Visual Studio Code”
wget https://quarto.org/download/latest/quarto-linux-amd64.deb
apt install -y ./quarto-linux-amd64.deb
Visit the following link to pick the latest link then run something similar to below
https://posit.co/download/shiny-server/
wget https://download3.rstudio.org/ubuntu-20.04/x86_64/shiny-server-1.5.23.1030-amd64.deb
gdebi -n shiny-server-1.5.23.1030-amd64.deb
Our shiny apps will be accessible through http://31.207.34.184:3838
More info
https://www.r-bloggers.com/2023/01/deploy-your-own-shiny-app-server-with-debian/
This will be our web server, we installed it above with all the system dependencies but now we need to start it, and make sure it starts by itself on reboot.
systemctl enable nginx
systemctl start nginx
The default nginx page will be at http://31.207.34.184
To easily transfer file from our server to our local machine it’s convenient to use a FTP client.
The 2 common options on mac are Cyberduck and Filezilla, we’ll go with cyberduck.
We install it and when we run it, define new collection as follows:
cyberduck
I go to VS code and create “/home/R/testproject/vibe.Rmd” and type this report, then knit it to create “/home/R/testproject/vibe.html”.
The DNS maps domain/subdomain names to IP addresses.
In our cases we want
vibe.m4i.be → 31.207.34.184
The DNS has no idea what “page” or “port” we’re using. It doesn’t handle routing to ports or URL paths, this will be handled on the server side.
For this I go to “DNS zone”, located in the panel in the m4i.be (domain) service, in the the “domain management” box.
There we:
TTL can stay at its default value. It stands for ‘Time To Live’ and determines how long the IP address is cached before being refreshed and is not all that relevant for us.
First we create a nginx config file.
The following command creates the file and open it for edition using “nano”, we can paste there the config that follows
nano /etc/nginx/sites-available/vibe.m4i.be
server {
# listen on port 80, standard for HTTP.
listen 80;
# server block that will handle requests
server_name vibe.m4i.be;
# where the server will look for files to serve
root /home/R/testproject;
# default index file when only sudomain.domain.ext is provided
index vibe.html;
# serve if exists, otherwise return a 404
location / {
try_files $uri $uri/ =404;
}
}
crl o , Enter, ctrl x
Note that this means in this case that every other file in the folder
will be accessible by vibe.m4i.be/my_file.ext (though the
listing is disabled by default so they wouldn’t be obvious to find from
the outside), see below to work around this.
server {
# listen on port 80, standard for HTTP.
listen 80;
# server block that will handle requests
server_name vibe.m4i.be;
# Allow access to only vibe.html
location = /vibe.html {
try_files $uri =404;
}
# Deny access to everything else
location / {
deny all;
}
}
Enable the site:
# create a symlink in /etc/nginx/sites-enabled/ pointing to the config we just defined
ln -s /etc/nginx/sites-available/vibe.m4i.be /etc/nginx/sites-enabled/
# test the nginx config for syntax errors and other issues
nginx -t
# reload the nginx service, applying the changes made to the configuration
systemctl reload nginx
Now we’re getting more ambitious and we want to use more technology.
We want to build :
But we also want it to be as simple as possible.
We already have some preinstalled with Shiny Server.
By default, apps are served from: “/srv/shiny-server/” and for our minimal example we can use “/srv/shiny-server/hello”.
Register a subdomain in the DNS just as we dide for “vibe” in the DNS zone but register hello.m4i.be this time.
As we did when serving our html page we create a configuration file and paste a config, which is a bit different this time.
nano /etc/nginx/sites-available/hello.m4i.be
server {
# listen on port 80, standard for HTTP.
listen 80;
# server block that will handle requests
server_name hello.m4i.be;
location / {
# forward all requests there
# 127.0.0.1 refers to the local machine
# Port 3838 is the default port for Shiny apps
proxy_pass http://127.0.0.1:3838/sample-apps/hello/;
# disables automatic redirection handling
proxy_redirect off;
# HTTP version for proxying, necessary for handling WebSocket connections that Shiny might use.
proxy_http_version 1.1;
# forward specific headers from the client to the backend service
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Anything after proxy_pass could be skipped, and I don’t understand much of it, but chatGPT recommands it so I comply.
Same as what we did for vibe, just changing the
name.
# create a symlink in /etc/nginx/sites-enabled/ pointing to the config we just defined
sudo ln -s /etc/nginx/sites-available/hello.m4i.be /etc/nginx/sites-enabled/
# test the nginx config for syntax errors and other issues
sudo nginx -t
# reload the nginx service, applying the changes made to the configuration
sudo systemctl reload nginx
Cmd Shift K
e.g. If the R terminal windows doesn’t want to start anymore.
pkill -f -u $EUID /code-server
| Part | Meaning |
|---|---|
pkill |
Sends a signal (default: TERM) to processes by name. |
-f |
Match against the full command line, not just the process name. |
-u $EUID |
Only kill processes owned by the current user (EUID = Effective UID). |
/code-server |
The string to match in the process command line. |
This only kills the process but it will restart automatically because it runs as a systemd service with restart policies. This was done in the “Create a systemd service for VS Code Server” in the installation steps above.