Super-Charged Remote Development with Tailscale and Jetbrains Projector

Ari Kalfus | Dec 7, 2021

This is the golden age to be in software development. There are so many opportunities and so many options for how to be productive. My laptop was aging, so I wanted to explore remote development products and compare the experience (and cost) before purchasing a new, beefy development machine.

I use a number of Jetbrains IDEs so my requirement was for a comparable remote development experience. My threshold for a new development laptop was $2,000, which is the cost of the new 14" Macbook Pro. As you may infer from the article title, I landed on Jetbrains Projector for my long-term remote development solution. While I was drafting this article, however, Jetbrains announced a new remote offering, Jetbrains Fleet. I’ll have to see how this new product develops and how it compares to Projector.

I ended up building two Ansible roles to quickly deploy a personal remote development server hosting arbitrary Jetbrains IDEs and keep it private and secure by accessing it through a Tailscale VPN. The process, from start to finish, takes less than 15 minutes to deploy this setup in a stable, long-term configuration.

I have tried AWS Cloud9 in the past and discarded it for my current comparison. It had too much friction for me. GitHub Codespaces was the primary platform to which I compared a self-hosted Jetbrains Projector environment.

I first take a look at how Codespaces and Projector compare from a cost perspective, then dive into how I configured Projector with Tailscale.

GitHub Codespaces

The strongest “managed” remote development option, in my opinion, is GitHub Codespaces. Again, this is coming out before I have access to Jetbrains Fleets, which is marketed as the managed option for Jetbrains IDEs, so that may change!

Codespaces immediately grabbed my attention when it went into beta, and I began experimenting with it. It provides instance access to an IDE in my browser synced with my preferences and plugins from Visual Studio Code. It has native integrations, which have gotten even better over time, to GitHub, where most of my code lives. It has continued to mature and is now one of my favorite places to work.

Odd way to start this article about Jetbrains, but I recommend everyone take a look at Codespaces as I think it is an extremely strong contender in this space of “remote development providers,” as long as GitHub is already your primary hosting platform. I also use GitHub Actions for my CI/CD platform, and Codespaces allows me to provide the same secrets to the Codespaces environment as in the dev environment in my CI suite.

GitHub Codespaces Pricing

I selected a 4-core, 8GB RAM machine, 10 hours of personal development per week, with 10 stopped Codespaces environments at any given time across my projects. I chose 5 GB as the average project size, although my local file system says my larger projects are only around 1 GB. Let’s say there’s a local database for testing. Individual GitHub user accounts are not currently billed, but I have a personal organization and I expect the user account billing will change in the future anyway. According to GitHub’s pricing calculator, my development costs could be between $14-$19/month, depending on how I toggle the details in the calculator. I will round up to $20.

For this article, let’s also say I massively increase my development footprint and my monthly costs become $50 for Codespaces. At these monthly prices, it would take 8.3 years ($20/month) or 3.33 years ($50/month) before my Codespaces usage equaled the initial $2,000 laptop investment. I try to keep my laptop around as long as it is useful to me, and they usually last 4-5 years. At 8 years (not adjusting for inflation, because I don’t want to), Codespaces is saving me a ton of money vs. purchasing an upfront development laptop. If I double or triple my development output, I creep up into breaking even. But I don’t think that is realistic.

Oh, and if I don’t round up and use $14/month? 11.9 years before I hit $2,000 and the original laptop investment! If I choose the 2 core, 4 GB RAM server? $11.30/month, and 14.7 years. That’s massive.

Jetbrains Projector

While I use Visual Studio Code and Jetbrains IDEs frequently, I prefer the Jetbrains products for most tasks. That’s my personal preference. Given that, I looked at Codespaces and wished there was a competing offering from Jetbrains. I was really excited to see Jetbrains Projector launch earlier this year as a self-hosted remote development option. Projector allows you to configure a Jetbrains IDE on a remote server and provides you a client (or in-browser experience) to use the IDE.

Jetbrains Projector project

I immediately began experimenting with it, and it provided everything I was looking for in my ideal development setup. Give me any old laptop, Chromebook, iPad, whatever, point it at my Projector URL, and I have immediate access to my full suite of development tools and Jetbrains’ Intellisense. There was very minor latency when performing actions like quickly scrolling down a large file, but nothing intrusive to my development experience, even when accessing the Projector IDE from hotel Wi-Fi. It felt like I was working with a local IDE on my machine!

So Projector is great and I want access to my IDEs from anywhere in the world, but I don’t want them exposed to everyone over the public internet. Jetbrains Projector has a command buried a bit in its documentation to set a “connection password.” This adds a query parameter that must exist to establish a connection to the IDE (e.g. https://myide.mysite.com/?token=MYTOKEN). This would work, but setting up HTTPS on the server in an automated fashion is a little more complicated, particularly for me who isn’t a regular JVM developer. The docs didn’t provide enough info for me to understand what to do and I didn’t want to spend a ton of time figuring it out. I also didn’t want to have to manually configure this every time I stood something up, and I wasn’t comfortable relying solely on the query parameter and using an HTTP site. There is an issue here on my Jetbrains Projector Ansible role to add support for “making the connection secure.” I would welcome collaboration! However, luckily, Tailscale solves this concern for us.

Enter Tailscale

Tailscale markets itself as a “zero config VPN” using WireGuard, and it is seriously awesome. I was looking for a modern, WireGuard alternative to my home OpenVPN setup. I had manually configured WireGuard on a few of my machines in the past but I didn’t enjoy the experience. Tailscale was magic. I would include a screenshot here but there’s really nothing to show. Tailscale just works.

I have been using Tailscale for some time to connect various personal machines, including a private Gollum wiki. Tailscale allows me to set up EC2 servers with a security group blocking all ingress traffic (allowing egress). Then I can access the servers from Tailscale’s IP addresses (e.g. 100.x.x.x) or using “magic DNS” to create hostnames that would resolve to my Tailscale infrastructure (such as the http://ides URL seen in the Safari screenshot above). Getting started with Tailscale is as simple as downloading the binary and running sudo tailscale up. I’ve been meaning to write an article to explain my setup in more detail, so consider that forthcoming.

Tailscale is magic and I highly recommend it. For our purposes, Tailscale will allow me to set up a Jetbrains Projector server on EC2 with no public exposure but let me seamlessly access it from any of my Tailscale-connected devices (via the private 100.x.x.x address or custom hostname).

Projector over Tailscale IP

Jetbrains Projector Pricing

Given these requirements, I’ll want at least 2 CPU cores and 4 GB RAM to self-host a Projector server. To compare equally with the Codespaces pricing, I’ll choose a server with a similar build - 4 cores and 8 GB RAM.

However, AWS doesn’t have a perfectly matching instance type. My best options are either a t4g.medium (2 cores, 4 GB RAM) or t4g.large (2 cores, 8 GB RAM). The next instance size, t4g.xlarge, is 4 cores with 16 GB RAM. I’ll go with the t4g.large for this calculation, although a t4g.medium has been sufficient for my personal use so far where I have been working on code in the IDE and haven’t needed a database or other process using a bunch of memory. I’ll attach a 50 GB GP3 volume to this instance. I have no idea how much to predict for inbound and outbound data transfer, so I put 10 GB/month. Is that wildly too high or wildly too low? Shrug. The 10 GB/month comes out to $0.81/month, and I can’t imagine it’d be much higher with personal use.

AWS’s “simple” pricing calculator tells me, at 10 hours of usage per week, this will cost me $7.82/month. Notably, this would require me to stop/start the EC2 instance when I wanted to use it. This is as simple as aws ec2 start-instances --instance-id $IDE_INSTANCE and aws ec2 stop-instances --instance-id $IDE_INSTANCE and is something I can readily do. I am also not configuring any EBS snapshots, relying on GitHub to store my code in commits and leveraging my Ansible role to rebuild a new server if something happens to the current one.

This is significantly less than $14-19/month with Codespaces, which makes me suspiciously think I missed something in the AWS pricing calculation. However, I would expect IaaS to cost less than PaaS, so this could be accurate. The AWS-hosted option requires me to configure the whole environment, after all, whereas Codespaces is an all-inclusive turnkey solution.

Luckily, my Ansible roles handle configuration :)

So, rounding to $8/month, how long could I use this configuration before meeting the cost of a $2,000 development laptop? 20.8 years. Ok, that got my attention.

Configuring a Jetbrains Remote IDE Server

If you want to skip straight to deploying your own private IDE server in 15 minutes, head over to the Ansible instructions. The next section will walk you through performing the actions taken inside the Ansible roles if you prefer a manual approach, or just want to understand what the roles are doing.

Installing Manually

First, create a Tailscale account. We have two options to connect our server to Tailscale. If you run sudo tailscale up your default browser will open and prompt you to authenticate the process to your Tailscale account. This is a simple, interactive way to connect your machine.

The non-interactive way is to use a pre-authentication key (Auth key), which you can generate from your account here. There are three auth key types: one-off keys, reusable keys, and ephemeral keys.

Tailscale Auth Keys

One-off keys are valid for one successful login and then are automatically invalidated. Reusable keys can be used as many times as you’d like for 90 days, at which point the key is invalidated and must be re-generated. Ephemeral keys act like reusable keys in that they are also valid for 90 days, however they produce ephemeral nodes. Ephemeral nodes get their access to your Tailscale account automatically revoked after a short time period of inactivity. That threshold is currently 48 hours, but I heard in some Tailscale ticket that this is moving to 2 hours soon (or may already be the case). In any event, their documentation (linked above) says ephemeral nodes can be auto-revoked anywhere from 30 minutes to 48 hours after the last activity on the node. Ephemeral auth keys are therefore super useful for temporary/testing infrastructure, such as the dev environment of your infrastructure-as-code pipeline or the CI suite of your Ansible role. The temporary infrastructure is automatically cleaned up and removed from your Tailscale account.

I recommend using an ephemeral auth key until you’ve settled on your infrastructure. Then you can run sudo tailscale logout and re-run sudo tailscale up with a one-off or reusable auth key.

After setting up your Tailscale account and authenticating your personal device, set up an EC2 server. I’m going to use the latest Amazon Linux 2 AMI on ARM, but the choice is up to you. Both Intel and ARM are supported by the Projector IDEs. I recommend using your infrastructure-as-code tool of choice, but for the purposes of this article I’m using the AWS Console.

I prefer to set up my AWS infrastructure without SSH keys and instead rely on AWS Systems Manager Session Manager (AWS SSM). How does that concatenate to SSM? Legacy documentation. If you’ve set up SSM in your AWS account previously, this means making sure to select an appropriate IAM role for the EC2 server. In this case, I’m using the AWS auto-created role when I set up SSM.

AWS SSM IAM Role

Since we plan on only connecting to this server over Tailscale’s VPN, I will configure the server with the default security group in the default VPC, allowing all traffic to and from other resources inside the default VPC security group only. If you’re in a private VPC without internet access, you should use a VPC Endpoint to connect SSM to your servers. You’re going to need to configure Tailscale to reach the private VPC as well. That’s outside the scope of this article. Don’t forget to launch the EC2 instance and “proceed without a key pair.”

When the instance comes up, grab the instance ID from the AWS Console and hop over to your CLI. I am a fan of using aws-vault to manage my AWS credentials, so I’m going to use that to connect to the new instance over SSM. If you’re not using aws-vault, don’t copy that part of the command! You can still use the regular aws ssm section.

AWS-Vault Login

If you go the SSM route, make sure you become the ec2-user before installing Projector. Some of the Python commands don’t like being ssm-user and will fail.

Manual - Tailscale

On our EC2 server, we want to 1) connect to Tailscale, and 2) install a Jetbrains Projector IDE.

Since we’re doing this manually, we’ll want to follow these instructions to install Tailscale on our chosen Linux distro. Since I’m using Amazon Linux 2, I’m following this.

sudo yum update
sudo yum install yum-utils
sudo yum-config-manager --add-repo https://pkgs.tailscale.com/stable/amazon-linux/2/tailscale.repo
sudo yum install tailscale
sudo systemctl enable --now tailscaled
sudo tailscale up --authkey <your_key>

That’s all you need to connect your server to Tailscale! You can get the private 100.x IP address of this server via the command tailscale ip -4. It is also available in your Tailscale account console.

Tailscale Machine Connected

Assuming you’ve configured Magic DNS, you can also give your server a customized hostname, such as ides.

Tailscale Customize Hostname

Try ping ides! (Your local machine must be authenticated to your Tailscale VPN as well.) This will make it easy to access our Jetbrains IDEs in a moment (e.g. http://ides:9876).

Manual - Jetbrains Projector

Installing and configuring the Jetbrains IDEs is a little more complicated. Installation instructions can be found on the JetBrains/projector-installer README, with additional instructions here. I’ve consolidated the instructions below. The additional items in my instructions vs. Jetbrains’ is to install gcc (required by Projector) and a Java JRE (which I guess Jetbrains assumes you already have). I’m also going to run these commands as the ec2-user instead of the ssm-user, as my intended owner of the IDE processes will be ec2-user.

sudo su ec2-user
# For Amazon Linux 2
sudo yum install -y freetype gcc less libXext libXrender libXtst libXi python3 python3-devel python3-pip python3-wheel pyOpenSSL python-cryptography
export PATH="$PATH:/home/ec2-user/.local/bin"
python3 -m pip install -U pip --user
# Install Jetbrains Projector
pip3 install projector-installer --user

Also install a Java 11 JRE of your choice.

  • Amazon Linux 2 - java-11-amazon-corretto
  • CentOS - java-11-openjdk
  • Debian/Ubuntu - openjdk-11-jre

NOTE! Remember to use a headed JRE. java-11-amazon-corretto-headless will produce errors :). It needs to run the IDE GUI, after all. I definitely didn’t make that mistake for 2 days.

sudo yum install -y java-11-amazon-corretto

If you’ve set the PATH as listed above and executed the commands as ec2-user, then you should now be able to run Jetbrains Projector via the projector executable.

Projector Version

We’re halfway there! Now to install and configure the IDEs of your choice.

First, determine the version of IDE you want to install. Let’s say we want to install WebStorm. Use projector ide find to search for valid installation versions. You need a full version string - e.g. WebStorm 2021.2.3 or IntelliJ IDEA Ultimate 2021.2.3. The ide find command is the easiest way to figure out what options are available. I find it helpful to install projector onto my local machine just to run ide find.

Projector IDE Find

I have not yet encountered any issues running any version of an IDE, but you may want to restrict your search to “Projector-tested IDEs only.” It’s a much smaller list:

Projector IDE Find Supported

Once you have selected an IDE and version string, you’re ready to begin the installation. To view all configuration options, you must pass the --expert flag. Pass the full version string you’ve selected.

projector ide install --expert "WebStorm 2021.2.3"

The --expert flag gives you the opportunity to specify whether you’d like to customize the listening address (which allows you to customize the port), provide custom hostnames, or provide a connection password (adding a required ?token=<your_password> query parameter).

If someone attempts to access your hostname without a correct token query parameter, the IDE will fail to establish a websocket connection.

Projector Missing Connection Password

With the correct token parameter, the IDE connection is established as normal.

Projector Connection Password

In the screenshot below, I added custom hostnames belonging to my machine’s configuration on Tailscale.

Projector IDE Install

At this point you can access http://ides:9999 (or whatever port was configured) from your Tailscale-connected personal machine and access WebStorm!

However, we’re not done just yet. The IDE is only accessible while our CLI process remains active. You can execute projector run WebStorm to start this process up in the future, but that’s inconvenient. To persist, we should configure a systemd service to run the IDE. For the service, we cannot use the projector run command. Instead, we want to call the underlying run.sh script.

I recommend the following:

[Unit]
Description=Jetbrains Projector - WebStorm

[Service]
Type=simple
ExecStart=/home/ec2-user/.projector/configs/WebStorm/run.sh
User=ec2-user
Group=ec2-user
Restart=always

[Install]
WantedBy=default.target

Add it with sudo vim /etc/systemd/system/WebStorm.service then run:

sudo systemctl daemon-reload
sudo systemctl enable WebStorm.service
sudo systemctl start WebStorm.service

You should now have a successful service running your IDE that will auto-start if the server reboots.

Projector Systemd

Below, I use the Tailscale IP address to access the server from a browser.

IDE Available

There is also a Projector Client if you prefer (download from the Releases tab or via Jetbrains Toolbox), but the experience is the same as in a browser.

Projector Client

We’re almost there! The final step to configuring the IDE is to activate it. Skip over the Ansible section to the IDE activation section to finish setting up your IDEs.

Installing With Ansible

I’ve created two roles, artis3n.tailscale and artis3n.jetbrains-projector, to install and configure Tailscale and Jetbrains Projector IDEs, respectively. These steps assume you have created some remote server (e.g. on EC2) and can SSH into it. If you’ve come from the manual section, marvel at how much less work this is!

Both roles are really robust, if I do say so myself. They each use Molecule for end-to-end tests in GitHub Action CI suites to ensure the roles correctly function against every supported operating system in each release. The Jetbrains Projector role implements a state caching mechanism bound to each IDE’s individual configuration so you have idempotency across your playbook (the Tailscale role is also fully idempotent). Both roles include validation tasks and will attempt to give clear, explicit messages upon an error.

Molecule Missing Auth Key Projector Missing Port

artis3n.tailscale is pretty simple to get started with, and allows you to get rather sophisticated if you need to set up subnet routes and traffic relays. The README contains all the details about the possible input variables you can configure. In the minimal case, which is how I use it, you only have to provide a Tailscale auth key. For example, if you were to provide the auth key as an environment variable to the process running Ansible:

---
- hosts: all

  roles:
    - role: artis3n.tailscale
      vars:
        tailscale_auth_key: "{{ ansible_env.TAILSCALE_KEY }}"

There is an example on the README if you need to configure more Tailscale parameters, such as subnet routes.

That’s it for Tailscale!

The Jetbrains Projector role also includes detailed descriptions of how to configure its input parameters on the README. You must tell the role what IDEs you’d like to install.

Minimally, you must supply the version string (e.g. WebStorm 2021.2.3 or IntelliJ IDEA Ultimate 2021.2.3) and a port for the IDE to be served from. The Jetbrains projector ide install command, when manually run, can auto-select a port on which to host the IDE. However, Ansible conventions promote explicit declarations and repeatable consistency. For this reason, you must choose a port yourself (and ensure nothing else is already using it).

- hosts: all
  roles:
    - role: artis3n.jetbrains_projector
      vars:
        ides:
          - name: WebStorm 2021.2.3
            port: 9876

The README contains some instructions to help you select an appropriate IDE version string, if needed. You can also require a connection password, set the listening address, and configure valid hostnames for each IDE. The manual section of this article goes into those options in a bit more depth. The role can also be used to cleanly uninstall any IDE that was previously installed with this role.

With your IDE configurations set, you are ready to run your playbook. A complete main.yml might look like:

---
- hosts: all

  roles:
    - role: artis3n.tailscale
      vars:
        tailscale_auth_key: "{{ ansible_env.TAILSCALE_KEY }}"

    - role: artis3n.jetbrains_projector
      vars:
        ides:
          - name: WebStorm 2021.2.3
            port: 9876

          - name: GoLand 2021.2.4
            port: 9878
            config: mygoland
            required_connection_password: secretpass

Create an inventory file to point to your server. You can alternatively choose to run Ansible from your remote server and execute the playbook against localhost. That inventory might look like:

[all]
127.0.0.1 ansible_connection=local ansible_user=ec2-user ansible_python_interpreter=/usr/bin/python3

Download my Ansible roles. Create a requirements.yml file:

---
 roles:
   - name: artis3n.tailscale
   - name: artis3n.jetbrains_projector

Download the roles:

ansible-galaxy install -r requirements.yml

Run the playbook!

ansible-playbook -i inventory main.yml

The process is captured in this recording:

Everything is now installed, configured, and ready for use! The final step is to access your IDEs and activate your Jetbrains subscription.

IDE Activation

Using a Projector IDE requires an active Jetbrains subscription. Regardless of whether you’ve configured things manually or via the Ansible role, your final step is to access the IDE and activate it under your Jetbrains account. I had some trouble following the prompts to get the Projector IDE to authenticate, however.

Projector Login

Clicking on Troubles? didn’t work for me either. I was unable to copy the provided hyperlink through the IDE in the browser or through Projector Client.

Projector Login Troubles

I ended up logging into my Jetbrains account and downloading an activation code.

Download Activation Jetbrains Enter Activation Code

That enabled me to authenticate and access the IDE.

Projector Login Success

Note: with Tailscale, you can access a localhost web server such as the above demo Vue app with the same hostname as you use to access the IDEs: http://ides:8080/.

Wrap-Up

While I covered a good amount of ground in this article, it should take you under 15 minutes to go from a routine server to a fully configured private IDE development environment secured behind a Tailscale VPN. It takes 2 minutes to run the Ansible playbook, after you’ve taken the time to configure how many IDEs you want hosted and with what configurations. My next steps are to wrap the server creation into Terraform, so you can deploy and configure the server and IDE software with one apply command. I look forward to seeing how Fleets disrupts the current environment as well. I think they have an uphill battle to overcome the native conveniences of Codespaces for people who use GitHub, however.

Have any questions about this setup? Reach out in the comments or file an issue on one of the Ansible role repositories!

comments powered by Disqus