Installing Actual Budget expense tracker in LXD and serving it using Tailscale with TLS.
I decided I want to try out tracking my expenses, and after some research I went with trying out Actual Budget. I’ve been using it for a few days now and the outlook is promising, I will report my experiences once I use it more.
However, I had some issues when setting it up with LXD so I am writing down how I worked out a working solution here for others to benefit from. And for me for the time when I want to install it again, having the steps written down will help a lot :)
Prelude
First, let’s sum up the situation. I have a homelab server running Debian 12. On that system I have LXD installed, LXD is an orchestrator for LXC VMs/system containers. Recently I have been preferring LXC from Docker, mostly for ease of backup and transfer of containers. As my homelab is moving to homeprod, I find continuity and stability of service to be the crucial part.
To access the services in the containers I am using static routing, the first method presented in this excellent video by Stéphane Graber. But in this case we will need this method only for local testing, later on we will move to accessing the container via Tailscale.
And so I want to install Actual Budget in an LXC container and be available even when I am away from my home network. I eventually went with the “Install from source” installation method described in the installation docs. I did not want to install it with Docker, and I could not make the “CLI Tool” method work for me in LXC.
Setting up the container
The description below is a result of several attempts of trial and error. A good thing with containers is that you can easily kill them and start anew. And I had to start from scratch a few times due to the requirements that Actual has.
Let’s start with creating a new container. In the LXD Web UI, create a new container, I am using the Ubuntu 24.04 image, but in this case any will do, as a long as it has systemd
. No special configuration needs to be done to the container. Once the container is started, go to the Terminal tab.
The next step is installing node
and npm
.
At first I installed node
using nvm
, but that caused issues with Actual, it could not find the node
binary, so I started again, and just downloaded the node
Standalone Binary (the green button on the download page) and copied it to /usr/bin
. When downloading node, the tar archive also has npm
. It can stay there, npm
is only used to install yarn
in our case.
Extract the downloaded archive, copy node
to /usr/bin
, and go the bin
folder in the downloaded tar archive where you will find npm
, and there run
npm install --global yarn
as described in the installation docs. yarn
will be installed globally and we will no longer need npm
.
Once yarn
is installed, the container is ready and we can move to installing Actual. go through the rest of the installation steps below the Installing Actual and Linux systemd setup headers.
At the moment of writing this blog post, there is a typo in the systemd instructions, the correct location of the service file is /etc/systemd/system/actual-server.service
.
Once the systemd service is up and running, you can access it via the IP address of the container (if you have set up static routing as I did, or by adding a proxy device listening on port 5006 of the container.
What you should see is an error page saying that you need HTTPS. Actual requires a HTTPS connection to work, there is no other way with it. So the next step is making your Actual container provide a HTTPS connection, and for that we will use Tailscale. Using Tailscale will have the added benefit of being able to access Actual from any place and not only our home network.
Setting up Tailscale
The steps will be very similar to what I wrote in the blog post about Nextcloud.
I recommend you go through it to understand the process, I won’t repeat here everything I wrote there.
We will install Tailscale in the container, install nginx, set it up as a reverse proxy for Actual, generate TLS certificates using Tailscale and add them to the nginx config. In case of Nextcloud, the reverse proxy server is Apache because this is what Nextcloud comes with, for Actual I am using nginx because it is the one I am most familiar with. But any server software capable of handling HTML with TLS will do.
First, install Tailscale in the container using the console. Tailscale provides a handy install script that can be generated at their admin page.
Once Tailscale is installed and authenticated, a new machine will appear in the admin panel of Tailscale. The new machine will have a URL that Tailscale calls the Magic DNS URL, we will need it in the next step.
The next step is generating TLS certificates for the server. It can be done by running
tailscale cert <your MagicDNS url for that machine>
The certificates will be generated right in the folder when you run this command so run it for example in /root
if you did not create a user othan than root. Running that command should create two files, *.crt
and *.key
.
With Tailscale part done, time to install and set up nginx. Nginx can be installed with apt
sudo apt install nginx
After installation nginx will start working right away and respond on the default port 80. You can confirm that by opening the container IP address in the browser without providing a port number, you should see the nginx default welcome screen.
Nginx installed, now it needs to be configured. Create a new file, called actual
in /etc/nginx/sites-available/
and paste:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name <CONTAINER_DOMAIN_URL>; # URL copied from Tailscale admin
ssl_certificate /root/<CONTAINER_DOMAIN_URL>.crt;
ssl_certificate_key /root/<CONTAINER_DOMAIN_URL>.key;
location / {
proxy_pass http://127.0.0.1:5006;
proxy_set_header Connection $http_connection;
proxy_set_header Upgrade $http_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;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 512M;
}
}
server {
listen 80 default_server;
listen [::]:80 default_server;
location / {
return 301 https://$host$request_uri;
}
}
Replace the URL with the one copied from the Tailscale admin. With the configuration created, it has to be applied so that nginx can use it:
cd /etc/nginx/sites-enabled
ln -s /etc/nginx/sites/availables/actual .
rm default
nginx -t
systemctl restart nginx
The link to the default file needs to be removed, and then the nginx configuration can be tested with nginx -t
. Once it is confirmed that the config is valid, restart the service to load the changes.
And that is all, you should now be able to access Actual in the browser by using the Tailscale machine URL. And it will work over HTTPS, making Actual happy. What is more, thanks to Tailscale, you will be able to use that URL on any device logged into the Tailnet, not only on your home network.
Bottom Line
I find LXD and Tailscale to be a very powerful combination, making running services straightforward, stable, and easily reachable from any place. So far I have used this solution to run Nextcloud, Actual, and Forgejo. I might do another blog post on Forgejo, how I have it configured together with Actions to have my own CI/CD pipelines locally.
I am also considering moving to Incus, as I heard good things about it and from what I found so far, the migration should not be that hard.
A real test of my LXD set up will be reinstalling my homelab base OS which I plan in the near future, mostly to free the server from all the crud and leftovers of failed experiments that I accrued over time. With that will come exporting and then importing again my LXC containers and I hope it will go smoothly.
And that’s basically it, thanks for reading!
If you enjoyed this post, please consider helping me make new projects by supporting me on the following crowdfunding sites: